mirror of
https://github.com/twentyhq/twenty.git
synced 2026-06-11 17:37:18 -04:00
@martmull v2.0 ;) --------- Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
361 lines
12 KiB
JavaScript
361 lines
12 KiB
JavaScript
const path = require('node:path');
|
|
|
|
const { PLUGIN_ROOT, readText } = require('./lib');
|
|
|
|
const assertTwentyMcpFormattingContract = (fail) => {
|
|
const skillPath = path.join(PLUGIN_ROOT, 'skills/use-twenty-mcp/SKILL.md');
|
|
const resultFormattingPath = path.join(PLUGIN_ROOT, 'references/use-twenty-mcp/result-formatting.md');
|
|
const skill = readText(skillPath);
|
|
const formatting = readText(resultFormattingPath);
|
|
|
|
const requiredSkillFragments = [
|
|
'# Output Contract',
|
|
'If the tool output includes `recordReferences`',
|
|
'MUST link each display name back to Twenty',
|
|
'{workspaceOrigin}/object/{objectNameSingular}/{recordId}',
|
|
'Never show unlinked record names',
|
|
];
|
|
|
|
for (const fragment of requiredSkillFragments) {
|
|
if (!skill.includes(fragment)) {
|
|
fail(`use-twenty-mcp/SKILL.md is missing formatting contract fragment: ${fragment}`);
|
|
}
|
|
}
|
|
|
|
const requiredFormattingFragments = [
|
|
'## Workspace Origin',
|
|
'derive the origin from it by removing the trailing `/mcp`',
|
|
'If `recordReferences` and workspace origin are both available',
|
|
'the first record-name column or record heading MUST link the display name',
|
|
'For recent companies with `recordReferences`, link the company name',
|
|
];
|
|
|
|
for (const fragment of requiredFormattingFragments) {
|
|
if (!formatting.includes(fragment)) {
|
|
fail(`result-formatting.md is missing record-link guidance fragment: ${fragment}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
const assertFrontComponentGuidance = (fail) => {
|
|
const developSkillPath = path.join(PLUGIN_ROOT, 'skills/develop-app/SKILL.md');
|
|
const layoutPath = path.join(PLUGIN_ROOT, 'references/develop-app/layout.md');
|
|
const frontComponentsPath = path.join(PLUGIN_ROOT, 'references/develop-app/front-components.md');
|
|
const standalonePagesPath = path.join(PLUGIN_ROOT, 'references/develop-app/standalone-pages.md');
|
|
const appStructurePath = path.join(PLUGIN_ROOT, 'references/develop-app/app-structure.md');
|
|
const frontComponentUiPath = path.join(PLUGIN_ROOT, 'references/design/front-component-ui.md');
|
|
const developSkill = readText(developSkillPath);
|
|
const layout = readText(layoutPath);
|
|
const frontComponents = readText(frontComponentsPath);
|
|
const standalonePages = readText(standalonePagesPath);
|
|
const appStructure = readText(appStructurePath);
|
|
const frontComponentUi = readText(frontComponentUiPath);
|
|
|
|
const requiredDevelopSkillFragments = [
|
|
'references/develop-app/front-components.md',
|
|
'references/develop-app/standalone-pages.md',
|
|
'Twenty UI imports',
|
|
'Use `layout.md` for placement, `standalone-pages.md` for full-page custom UI, and `front-component-ui.md` for visual design and Twenty UI component selection',
|
|
];
|
|
|
|
for (const fragment of requiredDevelopSkillFragments) {
|
|
if (!developSkill.includes(fragment)) {
|
|
fail(`develop-app/SKILL.md is missing front component guidance: ${fragment}`);
|
|
}
|
|
}
|
|
|
|
const requiredLayoutFragments = [
|
|
'## Front Component Widgets',
|
|
'frontComponentUniversalIdentifier',
|
|
'A `frontComponentId` is not the same value',
|
|
'use `front-components.md`',
|
|
];
|
|
|
|
for (const fragment of requiredLayoutFragments) {
|
|
if (!layout.includes(fragment)) {
|
|
fail(`layout.md is missing front component guidance: ${fragment}`);
|
|
}
|
|
}
|
|
|
|
const requiredFrontComponentFragments = [
|
|
'# Front Components',
|
|
'defineFrontComponent',
|
|
'Use `twenty-sdk/front-component`',
|
|
'Use `twenty-client-sdk/core` or `twenty-client-sdk/metadata`',
|
|
'Use `twenty-sdk/ui` for Twenty UI components',
|
|
'Do not import from `twenty-ui` directly',
|
|
'ThemeProvider',
|
|
'example-sources/twenty-ui-example.front-component.tsx',
|
|
'themeCssVariables',
|
|
'mocks `twenty-sdk/ui` during manifest extraction',
|
|
'A clean typecheck and sync is not runtime verification',
|
|
'without a `FrontComponent error`',
|
|
'hard refresh',
|
|
];
|
|
|
|
for (const fragment of requiredFrontComponentFragments) {
|
|
if (!frontComponents.includes(fragment)) {
|
|
fail(`front-components.md is missing runtime guidance: ${fragment}`);
|
|
}
|
|
}
|
|
|
|
const requiredStandalonePageFragments = [
|
|
'# Standalone Pages',
|
|
'custom page content should be rendered through a `FRONT_COMPONENT` widget',
|
|
'There does not appear to be a separate public "page body component" API',
|
|
"type: 'STANDALONE_PAGE'",
|
|
'NavigationMenuItemType.PAGE_LAYOUT',
|
|
'PageLayoutTabLayoutMode.CANVAS',
|
|
'gridPosition: { row: 0, column: 0, rowSpan: 12, columnSpan: 12 }',
|
|
'12 x 12 fill pattern',
|
|
'Full-Page Layout Guidance',
|
|
'black screen',
|
|
'yarn twenty dev --once',
|
|
];
|
|
|
|
for (const fragment of requiredStandalonePageFragments) {
|
|
if (!standalonePages.includes(fragment)) {
|
|
fail(`standalone-pages.md is missing standalone page guidance: ${fragment}`);
|
|
}
|
|
}
|
|
|
|
const requiredAppStructureFragments = [
|
|
'yarn twenty dev:typecheck',
|
|
'yarn lint',
|
|
'yarn twenty dev --once',
|
|
];
|
|
|
|
for (const fragment of requiredAppStructureFragments) {
|
|
if (!appStructure.includes(fragment)) {
|
|
fail(`app-structure.md is missing validation checklist command: ${fragment}`);
|
|
}
|
|
}
|
|
|
|
const requiredUiDesignFragments = [
|
|
'# Design Rules',
|
|
'Do not use this reference for source files, registration, runtime imports, data access, CLI commands, or browser verification',
|
|
'## Twenty UI Defaults',
|
|
'Prefer Twenty UI primitives',
|
|
'Use `Callout`',
|
|
'Use `Button`',
|
|
'Use `Tag`, `Status`, `Chip`, `Label`, and `Avatar`',
|
|
'Use `themeCssVariables`',
|
|
'Design the visible states',
|
|
];
|
|
|
|
for (const fragment of requiredUiDesignFragments) {
|
|
if (!frontComponentUi.includes(fragment)) {
|
|
fail(`front-component-ui.md is missing design-only guidance: ${fragment}`);
|
|
}
|
|
}
|
|
|
|
const forbiddenUiFragments = [
|
|
'# Runtime Safety',
|
|
'ReactCurrentDispatcher',
|
|
'yarn twenty',
|
|
'without a `FrontComponent error`',
|
|
];
|
|
|
|
for (const fragment of forbiddenUiFragments) {
|
|
if (frontComponentUi.includes(fragment)) {
|
|
fail(`front-component-ui.md should stay design-only and not include: ${fragment}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
const assertCliGuidanceSplit = (fail) => {
|
|
const developSkillPath = path.join(PLUGIN_ROOT, 'skills/develop-app/SKILL.md');
|
|
const manageSkillPath = path.join(PLUGIN_ROOT, 'skills/manage-app/SKILL.md');
|
|
const appStructurePath = path.join(PLUGIN_ROOT, 'references/develop-app/app-structure.md');
|
|
const cliAndSyncPath = path.join(PLUGIN_ROOT, 'references/manage-app/cli-and-sync.md');
|
|
const developSkill = readText(developSkillPath);
|
|
const manageSkill = readText(manageSkillPath);
|
|
const appStructure = readText(appStructurePath);
|
|
const cliAndSync = readText(cliAndSyncPath);
|
|
|
|
const requiredDevelopFragments = [
|
|
'references/develop-app/app-structure.md',
|
|
'yarn twenty dev:add',
|
|
'yarn twenty dev --once',
|
|
'switch to `manage-app`',
|
|
];
|
|
|
|
for (const fragment of requiredDevelopFragments) {
|
|
if (!developSkill.includes(fragment)) {
|
|
fail(`develop-app/SKILL.md is missing CLI split guidance: ${fragment}`);
|
|
}
|
|
}
|
|
|
|
const requiredManageFragments = [
|
|
'references/manage-app/cli-and-sync.md',
|
|
'validation command semantics',
|
|
'sync modes',
|
|
];
|
|
|
|
for (const fragment of requiredManageFragments) {
|
|
if (!manageSkill.includes(fragment)) {
|
|
fail(`manage-app/SKILL.md is missing CLI reference guidance: ${fragment}`);
|
|
}
|
|
}
|
|
|
|
const requiredAppStructureFragments = [
|
|
'# App Structure',
|
|
'../manage-app/cli-and-sync.md',
|
|
'## Entity Creation',
|
|
'## Validation Checklist',
|
|
'run lint and typecheck once at the end (not after each individual edit)',
|
|
'yarn twenty dev:typecheck',
|
|
'yarn lint',
|
|
'yarn twenty dev --once',
|
|
];
|
|
|
|
for (const fragment of requiredAppStructureFragments) {
|
|
if (!appStructure.includes(fragment)) {
|
|
fail(`app-structure.md is missing develop-app structure guidance: ${fragment}`);
|
|
}
|
|
}
|
|
|
|
const forbiddenAppStructureFragments = [
|
|
'# App Structure And CLI',
|
|
'Use watch mode only',
|
|
'Use watch mode for interactive development',
|
|
'Use one-shot mode for agents',
|
|
'yarn twenty dev --once --verbose',
|
|
'yarn twenty remote:list',
|
|
'Do not run `yarn twenty dev:typecheck`',
|
|
'run outside the sandbox',
|
|
'incompatible Node and Yarn',
|
|
];
|
|
|
|
for (const fragment of forbiddenAppStructureFragments) {
|
|
if (appStructure.includes(fragment)) {
|
|
fail(`app-structure.md should not own CLI semantics or forbid post-edit validation: ${fragment}`);
|
|
}
|
|
}
|
|
|
|
const requiredDevelopValidationFragments = [
|
|
'run lint and typecheck once at the end (not after each individual edit)',
|
|
'yarn twenty dev:typecheck',
|
|
'yarn lint',
|
|
];
|
|
|
|
for (const fragment of requiredDevelopValidationFragments) {
|
|
if (!developSkill.includes(fragment)) {
|
|
fail(`develop-app/SKILL.md is missing post-edit validation guidance: ${fragment}`);
|
|
}
|
|
}
|
|
|
|
const forbiddenDevelopFragments = [
|
|
'Do not run `yarn twenty dev:typecheck`',
|
|
'debug the toolchain',
|
|
'run outside the sandbox',
|
|
];
|
|
|
|
for (const fragment of forbiddenDevelopFragments) {
|
|
if (developSkill.includes(fragment)) {
|
|
fail(`develop-app/SKILL.md should not forbid post-edit validation or warn about the sandbox: ${fragment}`);
|
|
}
|
|
}
|
|
|
|
const requiredCliFragments = [
|
|
'# CLI And Sync',
|
|
'yarn twenty dev:typecheck',
|
|
'yarn lint',
|
|
'yarn twenty dev --once',
|
|
'Always use one-shot sync to synchronize app changes with the active remote',
|
|
'Do not use bare `yarn twenty dev` (watch mode)',
|
|
'yarn twenty dev --once --verbose',
|
|
'yarn twenty remote:list',
|
|
'yarn twenty dev:build',
|
|
'yarn twenty app:publish',
|
|
'yarn twenty dev:function:logs',
|
|
];
|
|
|
|
for (const fragment of requiredCliFragments) {
|
|
if (!cliAndSync.includes(fragment)) {
|
|
fail(`cli-and-sync.md is missing command guidance: ${fragment}`);
|
|
}
|
|
}
|
|
|
|
const forbiddenCliFragments = [
|
|
'run outside the sandbox',
|
|
'incompatible Node and Yarn',
|
|
'operations/command-execution.md',
|
|
];
|
|
|
|
for (const fragment of forbiddenCliFragments) {
|
|
if (cliAndSync.includes(fragment)) {
|
|
fail(`cli-and-sync.md should not warn about the sandbox or reference the removed command-execution.md: ${fragment}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
const assertTestingGuidance = (fail) => {
|
|
const manageSkillPath = path.join(PLUGIN_ROOT, 'skills/manage-app/SKILL.md');
|
|
const testsPath = path.join(PLUGIN_ROOT, 'references/develop-app/tests.md');
|
|
const cliAndSyncPath = path.join(PLUGIN_ROOT, 'references/manage-app/cli-and-sync.md');
|
|
const agentsPath = path.join(PLUGIN_ROOT, 'AGENTS.md');
|
|
const manageSkill = readText(manageSkillPath);
|
|
const tests = readText(testsPath);
|
|
const cliAndSync = readText(cliAndSyncPath);
|
|
const agents = readText(agentsPath);
|
|
|
|
const requiredManageFragments = [
|
|
'run tests for my Twenty app',
|
|
'references/develop-app/tests.md',
|
|
'yarn twenty docker:start --test',
|
|
'TWENTY_API_URL=http://localhost:2021 yarn test',
|
|
'Do not run integration tests against the dev instance on `http://localhost:2020`',
|
|
];
|
|
|
|
for (const fragment of requiredManageFragments) {
|
|
if (!manageSkill.includes(fragment)) {
|
|
fail(`manage-app/SKILL.md is missing test execution guidance: ${fragment}`);
|
|
}
|
|
}
|
|
|
|
const requiredSharedFragments = [
|
|
'isolated test instance',
|
|
'port (`2021`)',
|
|
'TWENTY_API_URL=http://localhost:2021 yarn test',
|
|
'Do not run integration tests against `http://localhost:2020`',
|
|
];
|
|
|
|
for (const fragment of requiredSharedFragments) {
|
|
if (!tests.includes(fragment)) {
|
|
fail(`tests.md is missing isolated integration-test guidance: ${fragment}`);
|
|
}
|
|
}
|
|
|
|
const requiredCliFragments = [
|
|
'Integration tests install and uninstall the app on their target server',
|
|
'yarn twenty docker:start --test',
|
|
'port `2021`',
|
|
'../develop-app/tests.md',
|
|
];
|
|
|
|
for (const fragment of requiredCliFragments) {
|
|
if (!cliAndSync.includes(fragment)) {
|
|
fail(`cli-and-sync.md is missing integration-test target guidance: ${fragment}`);
|
|
}
|
|
}
|
|
|
|
const requiredAgentsFragments = [
|
|
'TWENTY_API_URL=http://localhost:2021 yarn test',
|
|
'Integration tests must target the isolated test instance on port `2021`',
|
|
];
|
|
|
|
for (const fragment of requiredAgentsFragments) {
|
|
if (!agents.includes(fragment)) {
|
|
fail(`AGENTS.md is missing durable test target guidance: ${fragment}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
assertTwentyMcpFormattingContract,
|
|
assertFrontComponentGuidance,
|
|
assertCliGuidanceSplit,
|
|
assertTestingGuidance,
|
|
};
|