@open-mercato/cli 0.6.8-develop.6931.1.4a1e1c786b → 0.6.8-develop.6941.1.3c26284542

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/.turbo/turbo-build.log +64 -0
  2. package/build.mjs +57 -7
  3. package/dist/agentic/guides/backend-ui.md +7 -1
  4. package/dist/agentic/guides/module-facts.json +565 -224
  5. package/dist/agentic/guides/module-facts.v2.json +122933 -0
  6. package/dist/agentic/guides/modules/ai_assistant.md +2 -2
  7. package/dist/agentic/guides/modules/api_docs.md +2 -2
  8. package/dist/agentic/guides/modules/api_keys.md +2 -2
  9. package/dist/agentic/guides/modules/attachments.md +2 -2
  10. package/dist/agentic/guides/modules/audit_logs.md +2 -2
  11. package/dist/agentic/guides/modules/auth.md +2 -2
  12. package/dist/agentic/guides/modules/business_rules.md +2 -2
  13. package/dist/agentic/guides/modules/catalog.md +11 -4
  14. package/dist/agentic/guides/modules/channel_gmail.md +2 -2
  15. package/dist/agentic/guides/modules/channel_imap.md +2 -2
  16. package/dist/agentic/guides/modules/checkout.md +8 -2
  17. package/dist/agentic/guides/modules/communication_channels.md +6 -6
  18. package/dist/agentic/guides/modules/configs.md +2 -2
  19. package/dist/agentic/guides/modules/content.md +2 -2
  20. package/dist/agentic/guides/modules/currencies.md +2 -2
  21. package/dist/agentic/guides/modules/customer_accounts.md +2 -2
  22. package/dist/agentic/guides/modules/customers.md +8 -8
  23. package/dist/agentic/guides/modules/dashboards.md +12 -12
  24. package/dist/agentic/guides/modules/data_sync.md +4 -4
  25. package/dist/agentic/guides/modules/design_system.md +2 -2
  26. package/dist/agentic/guides/modules/dictionaries.md +2 -2
  27. package/dist/agentic/guides/modules/directory.md +2 -2
  28. package/dist/agentic/guides/modules/entities.md +3 -3
  29. package/dist/agentic/guides/modules/eudr.md +3 -3
  30. package/dist/agentic/guides/modules/events.md +2 -2
  31. package/dist/agentic/guides/modules/feature_toggles.md +2 -2
  32. package/dist/agentic/guides/modules/gateway_stripe.md +2 -2
  33. package/dist/agentic/guides/modules/generators.md +2 -2
  34. package/dist/agentic/guides/modules/inbox_ops.md +2 -2
  35. package/dist/agentic/guides/modules/integrations.md +5 -2
  36. package/dist/agentic/guides/modules/messages.md +2 -2
  37. package/dist/agentic/guides/modules/notifications.md +2 -2
  38. package/dist/agentic/guides/modules/onboarding.md +2 -2
  39. package/dist/agentic/guides/modules/payment_gateways.md +7 -3
  40. package/dist/agentic/guides/modules/perspectives.md +2 -2
  41. package/dist/agentic/guides/modules/planner.md +2 -2
  42. package/dist/agentic/guides/modules/portal.md +2 -2
  43. package/dist/agentic/guides/modules/progress.md +2 -2
  44. package/dist/agentic/guides/modules/query_index.md +2 -2
  45. package/dist/agentic/guides/modules/record_locks.md +3 -3
  46. package/dist/agentic/guides/modules/resources.md +2 -2
  47. package/dist/agentic/guides/modules/sales.md +13 -4
  48. package/dist/agentic/guides/modules/scheduler.md +2 -2
  49. package/dist/agentic/guides/modules/search.md +4 -4
  50. package/dist/agentic/guides/modules/security.md +3 -3
  51. package/dist/agentic/guides/modules/shipping_carriers.md +2 -2
  52. package/dist/agentic/guides/modules/sso.md +2 -2
  53. package/dist/agentic/guides/modules/staff.md +7 -4
  54. package/dist/agentic/guides/modules/storage_s3.md +2 -2
  55. package/dist/agentic/guides/modules/sync_akeneo.md +4 -4
  56. package/dist/agentic/guides/modules/sync_excel.md +2 -2
  57. package/dist/agentic/guides/modules/system_status_overlays.md +2 -2
  58. package/dist/agentic/guides/modules/translations.md +2 -2
  59. package/dist/agentic/guides/modules/webhooks.md +2 -2
  60. package/dist/agentic/guides/modules/wms.md +31 -4
  61. package/dist/agentic/guides/modules/workflows.md +2 -2
  62. package/dist/agentic/guides/reference-module-facts.json +8335 -0
  63. package/dist/agentic/guides/reference-modules/example.md +489 -0
  64. package/dist/agentic/guides/spec-delivery.md +34 -0
  65. package/dist/agentic/guides/testing-debugging.md +2 -1
  66. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +3 -2
  67. package/dist/agentic/guides/upstream/manifest.json +2 -2
  68. package/dist/agentic/shared/AGENTS.md.template +10 -14
  69. package/dist/agentic/shared/ai/harness/README.md +4 -4
  70. package/dist/agentic/shared/ai/harness/RELEASE.md +4 -4
  71. package/dist/agentic/shared/ai/harness/cases.json +11350 -5088
  72. package/dist/agentic/shared/ai/harness/cases.schema.json +70 -7
  73. package/dist/agentic/shared/ai/harness/design-system-inventory.json +6697 -0
  74. package/dist/agentic/shared/ai/harness/fixtures/index.json +3 -1
  75. package/dist/agentic/shared/ai/harness/fixtures/seeds.json +4 -0
  76. package/dist/agentic/shared/ai/harness/knowledge-change.schema.json +228 -0
  77. package/dist/agentic/shared/ai/harness/release-matrix.json +9 -4
  78. package/dist/agentic/shared/ai/harness/result.schema.json +60 -0
  79. package/dist/agentic/shared/ai/harness/routing-response.schema.json +10 -0
  80. package/dist/agentic/shared/ai/harness/source-link-inventory.json +3231 -0
  81. package/dist/agentic/shared/ai/harness/validators.json +8 -4
  82. package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +5 -2
  83. package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +3 -0
  84. package/dist/agentic/shared/ai/harness/writable-spec-oracles.mjs +879 -0
  85. package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.7-to-0.7.0/SKILL.md +102 -0
  86. package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +1 -0
  87. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +15 -0
  88. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +6 -0
  89. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +4 -0
  90. package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +1 -0
  91. package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +2 -0
  92. package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +2 -0
  93. package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +3 -1
  94. package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +1 -0
  95. package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +2 -0
  96. package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +2 -0
  97. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +3 -3
  98. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +1 -1
  99. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/knowledge-change.md +133 -0
  100. package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +1 -0
  101. package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +11 -0
  102. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +1 -1
  103. package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +2 -2
  104. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +14 -1
  105. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +2 -0
  106. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +19 -0
  107. package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +1 -4
  108. package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +1 -0
  109. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +24 -0
  110. package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +3 -1
  111. package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +1 -0
  112. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +13 -0
  113. package/dist/agentic/shared/ai/skills/tiers.json +2 -1
  114. package/dist/agentic/shared/ai/specs/2026-08-06-reference-module-activation.md +248 -0
  115. package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +39 -2
  116. package/dist/agentic/shared/scripts/design-source-contract.mjs +108 -0
  117. package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +935 -28
  118. package/dist/agentic/shared/scripts/framework-context.mjs +4 -1
  119. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +1 -1
  120. package/dist/agentic/shared/scripts/validate-knowledge-change.mjs +1511 -0
  121. package/dist/lib/__integration__/TC-INT-008.spec.js +1 -0
  122. package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
  123. package/dist/lib/agentic-setup.js +9 -1
  124. package/dist/lib/agentic-setup.js.map +2 -2
  125. package/dist/lib/generators/module-extension-facts.js +93 -26
  126. package/dist/lib/generators/module-extension-facts.js.map +3 -3
  127. package/dist/lib/generators/module-facts-discovery.js +23 -0
  128. package/dist/lib/generators/module-facts-discovery.js.map +2 -2
  129. package/dist/lib/generators/module-facts.js +969 -41
  130. package/dist/lib/generators/module-facts.js.map +2 -2
  131. package/dist/lib/generators/module-override-targets.js +35 -11
  132. package/dist/lib/generators/module-override-targets.js.map +2 -2
  133. package/dist/lib/generators/module-registry.js +105 -2
  134. package/dist/lib/generators/module-registry.js.map +2 -2
  135. package/dist/lib/resolver.js +5 -0
  136. package/dist/lib/resolver.js.map +2 -2
  137. package/dist/lib/testing/integration-discovery.js +34 -0
  138. package/dist/lib/testing/integration-discovery.js.map +2 -2
  139. package/dist/lib/testing/integration.js +0 -2
  140. package/dist/lib/testing/integration.js.map +2 -2
  141. package/dist/mercato.js +4 -4
  142. package/dist/mercato.js.map +2 -2
  143. package/package.json +6 -6
  144. package/src/lib/__integration__/TC-INT-008.spec.ts +4 -2
  145. package/src/lib/__tests__/resolve-environment.test.ts +18 -0
  146. package/src/lib/agentic-setup.ts +10 -2
  147. package/src/lib/generators/__tests__/helpers/override-target-diagnostic-fixtures.ts +150 -0
  148. package/src/lib/generators/__tests__/module-extension-facts.test.ts +290 -0
  149. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +72 -7
  150. package/src/lib/generators/__tests__/module-facts.computed-di-token.test.ts +88 -0
  151. package/src/lib/generators/__tests__/module-facts.example-fact-coverage.test.ts +502 -0
  152. package/src/lib/generators/__tests__/module-facts.local-reference.test.ts +414 -0
  153. package/src/lib/generators/__tests__/module-facts.search-entity-id.test.ts +151 -0
  154. package/src/lib/generators/__tests__/module-facts.source-links.test.ts +42 -1
  155. package/src/lib/generators/__tests__/module-override-targets.diagnostic-wiring.test.ts +92 -0
  156. package/src/lib/generators/__tests__/module-override-targets.test.ts +24 -0
  157. package/src/lib/generators/__tests__/module-override-targets.unknown-mode.test.ts +117 -0
  158. package/src/lib/generators/__tests__/module-subset.test.ts +60 -0
  159. package/src/lib/generators/module-extension-facts.ts +181 -25
  160. package/src/lib/generators/module-facts-discovery.ts +46 -0
  161. package/src/lib/generators/module-facts.ts +1330 -86
  162. package/src/lib/generators/module-override-targets.ts +70 -26
  163. package/src/lib/generators/module-registry.ts +135 -2
  164. package/src/lib/resolver.ts +6 -1
  165. package/src/lib/testing/__tests__/integration-discovery.test.ts +35 -0
  166. package/src/lib/testing/integration-discovery.ts +50 -0
  167. package/src/lib/testing/integration.ts +0 -2
  168. package/src/mercato.ts +3 -3
@@ -0,0 +1,879 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Fixed, controller-owned grader for the two SPEC-P2 writable planning proofs. The other two
4
+ // writable runners grade TypeScript: `writable-ast-oracles.mjs` reads a target's own compiler
5
+ // facts, `writable-behavior-oracles.mjs` executes a compiled seam. A planning proof produces
6
+ // no TypeScript at all — its only permitted artifact is Markdown under `.ai/specs/` — so it
7
+ // gets its own fixed runner rather than a Markdown branch bolted onto the AST oracle. The
8
+ // case table below is hard-coded exactly like the other two runners: a case cannot bring its
9
+ // own grading rules, and the evaluator resolves this file only for the two validator IDs it
10
+ // owns.
11
+
12
+ import fs from 'node:fs'
13
+ import path from 'node:path'
14
+ import process from 'node:process'
15
+ import { fileURLToPath } from 'node:url'
16
+
17
+ const EXIT_PASS = 0
18
+ const EXIT_FAILURE = 1
19
+ const EXIT_INVALID = 2
20
+
21
+ const SPEC_ROOT = '.ai/specs'
22
+ // Everything the scaffold itself emits under `.ai/specs`. None of these is an authored
23
+ // deliverable, so a planning proof neither counts them nor may repurpose them: the folder
24
+ // README, the blank skeleton, and the shipped covering specification for reference-module
25
+ // activation that gives the `reuse-spec` routing row an honest target.
26
+ const RESERVED_SPEC_FILES = Object.freeze([
27
+ 'README.md',
28
+ 'SPEC-000-template.md',
29
+ '2026-08-06-reference-module-activation.md',
30
+ ])
31
+ const SPEC_FILE_PATTERN = /^\d{4}-\d{2}-\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/
32
+ const MAX_SPEC_BYTES = 256 * 1024
33
+ const MAX_FACT_BYTES = 1024 * 1024
34
+ const MODULE_ROOT = 'src/modules'
35
+ const MODULE_REGISTRY = 'src/modules.ts'
36
+ const PLACEHOLDER_TOKEN_PATTERN = /(?:^|[^A-Za-z])(?:TBD|TODO|FIXME|XXX|lorem ipsum)(?:[^A-Za-z]|$)/
37
+ // Matches an unfilled `{…}` skeleton slot such as `{module}`, `{YYYY-MM-DD}`, or
38
+ // `{why it fits the platform}`, while leaving inline object prose like `{ items, totalCount }`
39
+ // alone: a real slot carries no comma and no padding space.
40
+ const CURLY_PLACEHOLDER_PATTERN = /\{(?!\s)[^{}\n,]{1,80}(?<!\s)\}/
41
+ const PHASE_HEADING_PATTERN = /^#{3,4}\s+Phase\s+(\d+)\b/i
42
+ const TEST_IDENTIFIER_PATTERN = /\bTEST-\d{3}\b|[\w./-]+\.(?:test|spec)\.tsx?\b|__integration__/
43
+
44
+ // --- module-shaped planning contract ------------------------------------------------------
45
+ // A module-shaped proof must ground its implementation in the reference module the app already
46
+ // ships rather than describing a second teaching module. These are the target-side facts the
47
+ // oracle reads to check that grounding; the plan under grading never supplies them.
48
+ const EXAMPLE_ROOT = 'src/modules/example'
49
+ const EXAMPLE_INVENTORY = `${EXAMPLE_ROOT}/references/surface-inventory.json`
50
+ const REFERENCE_FACTS = '.ai/guides/reference-module-facts.json'
51
+ const SOURCE_LINK_INVENTORY = '.ai/harness/source-link-inventory.json'
52
+ const REFERENCE_SHEET = '.ai/guides/reference-modules/example.md'
53
+ const EXAMPLE_FILE_PATTERN = /\bsrc\/modules\/example\/[A-Za-z0-9._/-]+\.[A-Za-z0-9]+\b/g
54
+ // A directory-only or globbed hint is not a source link. `src/modules/example/api/**`,
55
+ // `src/modules/example/widgets/*.ts` and a bare `src/modules/example/commands/` all name a
56
+ // place to look rather than a file to read, which is exactly what the read policy forbids.
57
+ const EXAMPLE_GLOB_PATTERN = /\bsrc\/modules\/example\/[A-Za-z0-9._/-]*(?:\*|\/(?=[\s`)|,.]|$))/
58
+ const AFTER_OPT_IN_PATTERN = /(?:source[-\s]present|not registered|unregistered|disabled|inert)[\s\S]{0,120}?(?:opt[-\s]?in|until it is registered|registration)|(?:opt[-\s]?in|register(?:ed|ing)?)[\s\S]{0,120}?(?:source[-\s]present|not registered|unregistered|disabled|inert)/i
59
+ const BULK_ACTION_CAPABILITY = 'umes.injection.datatable-bulk-action'
60
+ const BULK_PROGRESS_CAPABILITY = 'runtime.bulk-operation-progress'
61
+ const WORKFLOW_CAPABILITY = 'workflows.code-definition'
62
+ const BULK_CONTRIBUTION = 'example.injection.todo-bulk-complete@data-table:example.todos.list:bulk-actions'
63
+ const WORKFLOW_CONTRIBUTION = 'workflow:example.todo-created-reference'
64
+ const BULK_WORKERS = Object.freeze(['example:todos-bulk-dispatch', 'example:todos-bulk-complete'])
65
+ const REQUIRED_SOURCE_REFERENCES = Object.freeze([
66
+ 'om-module-scaffold/data-and-migrations:data/entities.ts',
67
+ 'om-module-scaffold/api-and-domain:commands/todos.ts',
68
+ 'om-backend-ui-design/crud-surfaces:widgets/injection/customer-priority-bulk-actions/widget.ts',
69
+ ])
70
+ const REQUIRED_DESIGN_REFERENCES = Object.freeze([
71
+ 'om-backend-ui-design/frontend-and-design-system:node_modules/@open-mercato/core/src/modules/design_system/gallery/entries/buttons.tsx',
72
+ 'om-backend-ui-design/frontend-and-design-system:node_modules/@open-mercato/ui/src/primitives/button.tsx',
73
+ 'om-backend-ui-design/frontend-and-design-system:node_modules/@open-mercato/ui/figma/button.figma.tsx',
74
+ 'om-backend-ui-design/frontend-and-design-system:src/app/globals.css',
75
+ ])
76
+ const REQUIRED_DESIGN_REFERENCE_ROLES = Object.freeze([
77
+ 'design-system-gallery',
78
+ 'design-system-implementation',
79
+ 'figma-code-connect',
80
+ 'design-foundation-token',
81
+ ])
82
+ const GALLERY_ITEM_ID = 'buttons/button'
83
+ const GALLERY_FAMILY_ID = 'buttons'
84
+ const GALLERY_ENTRY_ID = 'button'
85
+ const GALLERY_PACKAGE_SHA = 'bf25803d7a8c85c8552db9e76c7cc4398d1768be'
86
+ const FOUNDATION_AUDITED_SHA = 'fb9b8ddfe4470ef11d312caa4628c46af7d48adf'
87
+ const FOUNDATION_PACKAGE_SHA = 'b2d26489c683edc44265212ac8a79be1b981774f'
88
+ const UNSAFE_DESIGN_AUTHORITY_PATTERNS = Object.freeze([
89
+ { id: 'example-design-system-runtime-dependency', pattern: /(?:example[\s\S]{0,80}design_system|design_system[\s\S]{0,80}example)[\s\S]{0,80}(?:runtime dependency|depends on|import)/i },
90
+ { id: 'gallery-rule-owner', pattern: /gallery[\s\S]{0,80}(?:normative|rule owner|implementation contract)/i },
91
+ { id: 'figma-external-authority', pattern: /(?:fetch|request|use|obtain)[\s\S]{0,80}(?:figma credential|figma token)|(?:publish|push)[\s\S]{0,80}(?:code connect|figma)/i },
92
+ { id: 'workspace-token-source', pattern: /apps\/mercato\/src\/app\/globals\.css/i },
93
+ ])
94
+ const SHADOW_MODULE_PATTERNS = Object.freeze([
95
+ { id: 'shadow-teaching-module', pattern: /\b(?:new|second|another|own)\s+(?:teaching|example|demo|reference|sample)\s+module\b/i },
96
+ { id: 'whole-example-copy', pattern: /\bcopy(?:ing)?\s+(?:the\s+)?(?:whole|entire|complete|full)\s+(?:example|reference)\s+(?:module|tree|directory)\b|\bcp\s+-r\s+src\/modules\/example\b/i },
97
+ { id: 'ratelimit-probe-blueprint', pattern: /\bratelimit_probe\b/i },
98
+ ])
99
+ // Fixed by the spec-first routing contract, which owns this vocabulary. `pending-emission` is
100
+ // deliberately absent: it is a lifecycle marker on the canonical ledger, not a statement about
101
+ // which mechanism a proposed surface uses.
102
+ const MECHANISM_CLASSIFICATIONS = Object.freeze([
103
+ 'emitted-example',
104
+ 'framework-only',
105
+ 'catalog-only',
106
+ 'currently-unbound',
107
+ 'negative-fixture',
108
+ ])
109
+ // Activated canonical output may never claim this one: only a deliberately broken fixture
110
+ // produces it, so a plan that proposes it is proposing a defect.
111
+ const FORBIDDEN_CLASSIFICATION = 'negative-fixture'
112
+ const TRACEABILITY_HEADING_PATTERN = /traceab/i
113
+ const REQUIREMENT_ID_PATTERN = /\bREQ-\d{3}\b/
114
+ const PHASE_REFERENCE_PATTERN = /\bPhase\s+\d+\b/i
115
+ // Reserved scaffolding must still look like scaffolding afterwards. Byte equality against the
116
+ // controller is not usable: `.ai/specs/README.md` carries the generated project name, which
117
+ // legitimately differs between the controller app and the disposable target.
118
+ const RESERVED_SPEC_MARKERS = Object.freeze({
119
+ 'README.md': /^#\s+Feature Specs/m,
120
+ 'SPEC-000-template.md': /\{Title\}/,
121
+ '2026-08-06-reference-module-activation.md': /^#\s+Reference Module Activation/m,
122
+ })
123
+
124
+ // Each requirement resolves to one distinct level-2 section, preferring the exact heading the
125
+ // scaffold's own skeleton ships (`.ai/specs/SPEC-000-template.md`) before looser synonyms, so a
126
+ // plan written from the routed template satisfies it without renaming anything.
127
+ const REQUIRED_SECTIONS = Object.freeze([
128
+ {
129
+ id: 'problem',
130
+ requirement: 'a problem statement',
131
+ patterns: [/^problem statement$/i, /problem/i, /background/i],
132
+ minWords: 80,
133
+ },
134
+ {
135
+ id: 'contracts',
136
+ requirement: 'a contract section',
137
+ patterns: [/api[,\s].*contract/i, /^data models?$/i, /contract/i, /\bapi\b/i],
138
+ minWords: 80,
139
+ },
140
+ {
141
+ id: 'tests',
142
+ requirement: 'a test coverage section',
143
+ patterns: [/integration coverage/i, /^tests?\b/i, /test/i, /verification/i],
144
+ minWords: 40,
145
+ },
146
+ {
147
+ id: 'phases',
148
+ requirement: 'a phased implementation section',
149
+ patterns: [/implementation phases?/i, /implementation plan/i, /phase/i, /implementation/i],
150
+ minWords: 80,
151
+ },
152
+ ])
153
+
154
+ const SPEC_CASES = Object.freeze({
155
+ 'OMH-223': {
156
+ kind: 'new-feature',
157
+ // The ordering proof is that planning delivered one covering spec and no implementation.
158
+ // These are the module-directory names a warehouse-transfer implementation would occupy.
159
+ implementationKeywords: ['transfer', 'warehouse'],
160
+ // Stock transfers are a module, so this proof is also the module-shaped one: its plan has
161
+ // to route implementation to the scaffold skill and name the exact reference sources it
162
+ // will adapt, rather than describing a second teaching module in prose.
163
+ moduleShaped: {
164
+ scaffoldSkill: 'om-module-scaffold',
165
+ minCapabilityIds: 2,
166
+ minExactSourcePaths: 2,
167
+ minTraceabilityRows: 5,
168
+ },
169
+ },
170
+ 'OMH-224': {
171
+ kind: 'existing-spec',
172
+ coveringSpec: `${SPEC_ROOT}/2026-08-04-service-appointment-reminders.md`,
173
+ // Fixed expectations for the contract change the prompt introduces. Hard-coded here, never
174
+ // derived from the seeded file or from the answer under grading.
175
+ amendments: [
176
+ {
177
+ id: 'rescheduled-and-cancelled',
178
+ requirement: 'the amended contract must cover rescheduled and cancelled appointments',
179
+ all: [/reschedul/i, /cancel/i],
180
+ },
181
+ {
182
+ id: 'per-channel-outcome',
183
+ requirement: 'the amended contract must record a per-channel delivery outcome including SMS',
184
+ all: [/\bsms\b/i, /per[-\s]channel|delivery (?:outcome|status|result)/i],
185
+ },
186
+ ],
187
+ // The amendment must not silently drop the behavior the covering spec already committed to.
188
+ preserved: [
189
+ {
190
+ id: 'confirmed-reminder',
191
+ requirement: 'the original confirmed-appointment reminder contract must survive the amendment',
192
+ pattern: /confirmed appointment/i,
193
+ },
194
+ ],
195
+ minChangelogEntries: 2,
196
+ implementationKeywords: ['reminder', 'appointment'],
197
+ },
198
+ })
199
+
200
+ function check(id, passed, requirement) {
201
+ return { id, passed, requirement }
202
+ }
203
+
204
+ function isSafeRelative(relative) {
205
+ if (typeof relative !== 'string' || !relative || path.isAbsolute(relative) || relative.includes('\0')) return false
206
+ return !relative.replaceAll('\\', '/').split('/').includes('..')
207
+ }
208
+
209
+ function readContainedFile(root, relative, maxBytes = MAX_SPEC_BYTES) {
210
+ if (!isSafeRelative(relative)) return null
211
+ const lexical = path.resolve(root, relative)
212
+ if (path.relative(root, lexical).startsWith('..')) return null
213
+ let stat
214
+ try { stat = fs.lstatSync(lexical) } catch { return null }
215
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > maxBytes) return null
216
+ try { return fs.readFileSync(lexical, 'utf8') } catch { return null }
217
+ }
218
+
219
+ function listSpecFiles(root) {
220
+ const directory = path.join(root, ...SPEC_ROOT.split('/'))
221
+ let entries
222
+ try { entries = fs.readdirSync(directory, { withFileTypes: true }) } catch { return [] }
223
+ return entries
224
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.md'))
225
+ .map((entry) => entry.name)
226
+ .sort()
227
+ }
228
+
229
+ function authoredSpecFiles(root) {
230
+ return listSpecFiles(root).filter((name) => !RESERVED_SPEC_FILES.includes(name))
231
+ }
232
+
233
+ // Splits a Markdown document into its level-2 sections, ignoring headings inside fenced code
234
+ // blocks so an ASCII wireframe cannot fabricate a section.
235
+ function level2Sections(markdown) {
236
+ const sections = []
237
+ let fenced = false
238
+ let current = null
239
+ for (const line of markdown.split(/\r?\n/)) {
240
+ if (/^\s*(?:```|~~~)/.test(line)) fenced = !fenced
241
+ if (!fenced) {
242
+ const heading = /^##\s+(.+?)\s*$/.exec(line)
243
+ if (heading) {
244
+ current = { heading: heading[1], lines: [] }
245
+ sections.push(current)
246
+ continue
247
+ }
248
+ }
249
+ if (current) current.lines.push(line)
250
+ }
251
+ return sections.map((section) => ({ heading: section.heading, body: section.lines.join('\n') }))
252
+ }
253
+
254
+ function strippedOfCodeBlocks(body) {
255
+ const kept = []
256
+ let fenced = false
257
+ for (const line of body.split(/\r?\n/)) {
258
+ if (/^\s*(?:```|~~~)/.test(line)) { fenced = !fenced; continue }
259
+ if (!fenced) kept.push(line)
260
+ }
261
+ return kept.join('\n')
262
+ }
263
+
264
+ function wordCount(body) {
265
+ return strippedOfCodeBlocks(body).split(/\s+/).filter((token) => /[A-Za-z0-9]/.test(token)).length
266
+ }
267
+
268
+ function placeholderReason(body) {
269
+ const prose = strippedOfCodeBlocks(body)
270
+ if (PLACEHOLDER_TOKEN_PATTERN.test(prose)) return 'an unresolved TODO/TBD marker'
271
+ if (CURLY_PLACEHOLDER_PATTERN.test(prose)) return 'an unfilled {…} skeleton slot'
272
+ return null
273
+ }
274
+
275
+ function orderedPhaseNumbers(body) {
276
+ const numbers = []
277
+ let fenced = false
278
+ for (const line of body.split(/\r?\n/)) {
279
+ if (/^\s*(?:```|~~~)/.test(line)) { fenced = !fenced; continue }
280
+ if (fenced) continue
281
+ const match = PHASE_HEADING_PATTERN.exec(line)
282
+ if (match) numbers.push(Number(match[1]))
283
+ }
284
+ return numbers
285
+ }
286
+
287
+ function changelogEntryCount(markdown) {
288
+ const section = level2Sections(markdown).find((entry) => /changelog/i.test(entry.heading))
289
+ if (!section) return 0
290
+ const lines = strippedOfCodeBlocks(section.body).split(/\r?\n/)
291
+ const rows = lines.filter((line) => /^\s*\|/.test(line)
292
+ && !/^\s*\|[\s:|-]*\|\s*$/.test(line)
293
+ && !/^\s*\|\s*date\s*\|/i.test(line))
294
+ const bullets = lines.filter((line) => /^\s*[-*]\s+\S/.test(line))
295
+ return rows.length + bullets.length
296
+ }
297
+
298
+ function resolveRequiredSections(markdown) {
299
+ const sections = level2Sections(markdown)
300
+ const claimed = new Set()
301
+ const resolved = new Map()
302
+ for (const requirement of REQUIRED_SECTIONS) {
303
+ let match
304
+ for (const pattern of requirement.patterns) {
305
+ match = sections.find((section) => !claimed.has(section) && pattern.test(section.heading))
306
+ if (match) break
307
+ }
308
+ if (match) claimed.add(match)
309
+ resolved.set(requirement.id, match ?? null)
310
+ }
311
+ return resolved
312
+ }
313
+
314
+ function specStructureChecks(markdown) {
315
+ const checks = []
316
+ const resolved = resolveRequiredSections(markdown)
317
+ for (const requirement of REQUIRED_SECTIONS) {
318
+ const match = resolved.get(requirement.id)
319
+ if (!match) {
320
+ checks.push(check(`spec.section.${requirement.id}`, false, `the specification needs ${requirement.requirement}`))
321
+ continue
322
+ }
323
+ const words = wordCount(match.body)
324
+ checks.push(check(
325
+ `spec.section.${requirement.id}`,
326
+ words >= requirement.minWords,
327
+ `${requirement.requirement} needs at least ${requirement.minWords} words of substance (found ${words} under "${match.heading}")`,
328
+ ))
329
+ const placeholder = placeholderReason(match.body)
330
+ checks.push(check(
331
+ `spec.substantive.${requirement.id}`,
332
+ placeholder === null,
333
+ `${requirement.requirement} must be written, not templated${placeholder ? ` (found ${placeholder})` : ''}`,
334
+ ))
335
+ if (requirement.id === 'phases') {
336
+ const phases = orderedPhaseNumbers(match.body)
337
+ checks.push(check(
338
+ 'spec.phases.ordered',
339
+ phases.length >= 2 && phases.every((value, index) => value === index + 1),
340
+ `the implementation section needs at least two dependency-ordered phases numbered from 1 (found ${phases.join(', ') || 'none'})`,
341
+ ))
342
+ }
343
+ if (requirement.id === 'tests') {
344
+ checks.push(check(
345
+ 'spec.tests.identified',
346
+ TEST_IDENTIFIER_PATTERN.test(strippedOfCodeBlocks(match.body)),
347
+ 'the test section must name concrete test IDs or test file paths',
348
+ ))
349
+ }
350
+ }
351
+ return checks
352
+ }
353
+
354
+ function reservedSpecChecks(root) {
355
+ return RESERVED_SPEC_FILES.map((name) => {
356
+ const contents = readContainedFile(root, `${SPEC_ROOT}/${name}`)
357
+ return check(
358
+ `reserved.${name}`,
359
+ contents !== null && RESERVED_SPEC_MARKERS[name].test(contents),
360
+ `${SPEC_ROOT}/${name} must remain the emitted scaffolding, not the delivered specification`,
361
+ )
362
+ })
363
+ }
364
+
365
+ function implementationChecks(root, definition) {
366
+ const keywords = definition.implementationKeywords
367
+ const moduleDirectory = path.join(root, ...MODULE_ROOT.split('/'))
368
+ let moduleNames = []
369
+ try {
370
+ moduleNames = fs.readdirSync(moduleDirectory, { withFileTypes: true })
371
+ .filter((entry) => entry.isDirectory())
372
+ .map((entry) => entry.name.toLowerCase())
373
+ } catch { moduleNames = [] }
374
+ const registry = (readContainedFile(root, MODULE_REGISTRY) ?? '').toLowerCase()
375
+ const implicated = keywords.filter((keyword) => moduleNames.some((name) => name.includes(keyword)) || registry.includes(keyword))
376
+ return [check(
377
+ 'planning.implementation-absent',
378
+ implicated.length === 0,
379
+ `planning must not start implementation: no module directory or registry entry may name ${keywords.join(' or ')}${implicated.length ? ` (found ${implicated.join(', ')})` : ''}`,
380
+ )]
381
+ }
382
+
383
+ // The capability IDs and exact source paths the target's own reference module publishes. Read
384
+ // from the target, never from the plan: a plan cannot widen its own grounding by asserting a
385
+ // capability the shipped inventory does not carry.
386
+ function exampleGrounding(root) {
387
+ const raw = readContainedFile(root, EXAMPLE_INVENTORY)
388
+ if (raw === null) return null
389
+ let parsed
390
+ try { parsed = JSON.parse(raw) } catch { return null }
391
+ const capabilities = Array.isArray(parsed?.capabilities) ? parsed.capabilities : []
392
+ if (!capabilities.length) return null
393
+ const capabilityIds = new Set()
394
+ const sourcePaths = new Set()
395
+ for (const capability of capabilities) {
396
+ if (typeof capability?.capabilityId === 'string') capabilityIds.add(capability.capabilityId)
397
+ for (const relative of capability?.sourcePaths ?? []) if (typeof relative === 'string') sourcePaths.add(relative)
398
+ }
399
+ return { capabilityIds, sourcePaths, inventory: parsed }
400
+ }
401
+
402
+ function readJsonObject(root, relative) {
403
+ const raw = readContainedFile(root, relative, MAX_FACT_BYTES)
404
+ if (raw === null) return null
405
+ try {
406
+ const parsed = JSON.parse(raw)
407
+ return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null
408
+ } catch {
409
+ return null
410
+ }
411
+ }
412
+
413
+ function generatedReferenceGrounding(root) {
414
+ const bundle = readJsonObject(root, REFERENCE_FACTS)
415
+ const reference = bundle?.example
416
+ const facts = reference?.facts
417
+ if (
418
+ reference?.projectionKind !== 'activated-reference'
419
+ || reference?.sourceKind !== 'local-reference'
420
+ || reference?.runtimeSelected !== false
421
+ || facts === null
422
+ || typeof facts !== 'object'
423
+ ) return null
424
+
425
+ const contributions = Array.isArray(facts.extensionSurfaces?.contributions)
426
+ ? facts.extensionSurfaces.contributions
427
+ : []
428
+ const resolutions = Array.isArray(facts.extensionSurfaces?.contributionResolutions)
429
+ ? facts.extensionSurfaces.contributionResolutions
430
+ : []
431
+ const activations = Array.isArray(facts.extensionSurfaces?.activations)
432
+ ? facts.extensionSurfaces.activations
433
+ : []
434
+ const overrideTargets = Array.isArray(facts.overrideTargets) ? facts.overrideTargets : []
435
+ const workers = Array.isArray(facts.ownedContracts?.worker) ? facts.ownedContracts.worker : []
436
+
437
+ const bulkContribution = contributions.find((entry) => entry?.id === BULK_CONTRIBUTION)
438
+ const workflowContribution = contributions.find((entry) => entry?.id === WORKFLOW_CONTRIBUTION)
439
+ const bulkResolution = resolutions.find((entry) => entry?.contributionId === BULK_CONTRIBUTION)
440
+ const activationIds = Array.isArray(bulkResolution?.activationIds) ? bulkResolution.activationIds : []
441
+ const activationSet = new Set(activations.map((entry) => entry?.id).filter((id) => typeof id === 'string'))
442
+ const workerIds = BULK_WORKERS.filter((id) => workers.some((entry) => entry?.id === id))
443
+ const relevantFactKeys = new Set([BULK_CONTRIBUTION, ...workerIds])
444
+ const overrideIds = overrideTargets
445
+ .filter((entry) => relevantFactKeys.has(entry?.factRef?.factKey))
446
+ .map((entry) => entry?.id)
447
+ .filter((id) => typeof id === 'string')
448
+
449
+ if (
450
+ !bulkContribution
451
+ || !workflowContribution
452
+ || workflowContribution?.details?.specialistRoute !== 'workflows'
453
+ || activationIds.length === 0
454
+ || !activationIds.every((id) => activationSet.has(id))
455
+ || workerIds.length !== BULK_WORKERS.length
456
+ || overrideIds.length < 3
457
+ ) return null
458
+
459
+ return {
460
+ activationIds,
461
+ overrideIds,
462
+ workerIds,
463
+ specialistRoute: workflowContribution.details.specialistRoute,
464
+ workflowSource: workflowContribution?.source?.path,
465
+ }
466
+ }
467
+
468
+ function sourceReferenceGrounding(root) {
469
+ const inventory = readJsonObject(root, SOURCE_LINK_INVENTORY)
470
+ const records = Array.isArray(inventory?.records) ? inventory.records : []
471
+ const byId = new Map(records
472
+ .filter((entry) => typeof entry?.referenceId === 'string')
473
+ .map((entry) => [entry.referenceId, entry]))
474
+ const requiredIds = [...REQUIRED_SOURCE_REFERENCES, ...REQUIRED_DESIGN_REFERENCES]
475
+ if (!requiredIds.every((id) => byId.get(id)?.readStatus === 'readable')) return null
476
+ if (!REQUIRED_DESIGN_REFERENCES.every((id, index) => byId.get(id)?.referenceRole === REQUIRED_DESIGN_REFERENCE_ROLES[index])) {
477
+ return null
478
+ }
479
+ const visual = REQUIRED_DESIGN_REFERENCES
480
+ .flatMap((id) => Array.isArray(byId.get(id)?.visualReferences) ? byId.get(id).visualReferences : [])
481
+ .find((entry) => entry?.galleryItemId === GALLERY_ITEM_ID)
482
+ if (visual?.familyId !== GALLERY_FAMILY_ID || visual?.entryId !== GALLERY_ENTRY_ID) return null
483
+ return { byId, visual }
484
+ }
485
+
486
+ function exactIdentityChecks(prose, values, prefix, requirement) {
487
+ const missing = values.filter((value) => typeof value !== 'string' || !prose.includes(value))
488
+ return check(
489
+ prefix,
490
+ missing.length === 0,
491
+ `${requirement}${missing.length ? ` (missing ${missing.join(', ')})` : ''}`,
492
+ )
493
+ }
494
+
495
+ // Splits every Markdown table in a section into its data rows. Header detection follows the
496
+ // grammar rather than the wording: a header is the line immediately above a delimiter row, so a
497
+ // data row whose first cell reads `REQ-001` is never mistaken for one.
498
+ function tableRows(body) {
499
+ const lines = body.split(/\r?\n/).map((line) => line.trim())
500
+ const isRow = (line) => line.startsWith('|') && line.endsWith('|') && line.length > 2
501
+ const cellsOf = (line) => line.slice(1, -1).split('|').map((cell) => cell.trim())
502
+ const isDelimiter = (line) => isRow(line) && cellsOf(line).every((cell) => /^:?-{2,}:?$/.test(cell))
503
+ const rows = []
504
+ for (let index = 0; index < lines.length; index += 1) {
505
+ if (!isRow(lines[index]) || isDelimiter(lines[index])) continue
506
+ // The header sits directly above the delimiter; skip both and read the body that follows.
507
+ if (index + 1 < lines.length && isDelimiter(lines[index + 1])) { index += 1; continue }
508
+ const cells = cellsOf(lines[index])
509
+ if (cells.length < 3) continue
510
+ rows.push({ cells, text: lines[index] })
511
+ }
512
+ return rows
513
+ }
514
+
515
+ function moduleShapedChecks(root, markdown, contract) {
516
+ const checks = []
517
+ const resolved = resolveRequiredSections(markdown)
518
+ const implementation = resolved.get('phases')
519
+ const prose = strippedOfCodeBlocks(markdown)
520
+
521
+ checks.push(check(
522
+ 'plan.module-scaffold-route',
523
+ implementation !== null && implementation !== undefined && implementation.body.includes(contract.scaffoldSkill),
524
+ `the implementation section must route module work to \`${contract.scaffoldSkill}\``,
525
+ ))
526
+
527
+ const grounding = exampleGrounding(root)
528
+ if (grounding === null) {
529
+ checks.push(check(
530
+ 'plan.example-inventory-readable',
531
+ false,
532
+ `the target must ship ${EXAMPLE_INVENTORY}; without it no plan can be checked against the reference module`,
533
+ ))
534
+ return checks
535
+ }
536
+
537
+ const citedCapabilities = [...grounding.capabilityIds].filter((id) => prose.includes(id))
538
+ checks.push(check(
539
+ 'plan.example-capability-ids',
540
+ citedCapabilities.length >= contract.minCapabilityIds,
541
+ `the plan must name at least ${contract.minCapabilityIds} capability IDs published by ${EXAMPLE_INVENTORY} (found ${citedCapabilities.length ? citedCapabilities.join(', ') : 'none'})`,
542
+ ))
543
+
544
+ const cited = [...new Set(prose.match(EXAMPLE_FILE_PATTERN) ?? [])]
545
+ const grounded = cited.filter((relative) => grounding.sourcePaths.has(relative) && readContainedFile(root, relative) !== null)
546
+ checks.push(check(
547
+ 'plan.example-exact-source-paths',
548
+ grounded.length >= contract.minExactSourcePaths,
549
+ `the plan must name at least ${contract.minExactSourcePaths} exact reference source files that exist and are mapped by the inventory (found ${grounded.length ? grounded.join(', ') : 'none'})`,
550
+ ))
551
+ const ungrounded = cited.filter((relative) => !grounding.sourcePaths.has(relative))
552
+ checks.push(check(
553
+ 'plan.example-paths-resolve',
554
+ ungrounded.length === 0,
555
+ `every named reference source must exist and be mapped by the inventory (unmapped: ${ungrounded.join(', ')})`,
556
+ ))
557
+ const glob = EXAMPLE_GLOB_PATTERN.exec(prose)
558
+ checks.push(check(
559
+ 'plan.example-no-directory-hint',
560
+ glob === null,
561
+ `reference sources must be exact files, not a directory or wildcard${glob ? ` (found ${glob[0]})` : ''}`,
562
+ ))
563
+
564
+ checks.push(check(
565
+ 'plan.example-after-opt-in',
566
+ AFTER_OPT_IN_PATTERN.test(prose),
567
+ 'the plan must state that the reference module is source-present and applies only after opt-in, not that it is already live',
568
+ ))
569
+
570
+ for (const { id, pattern } of SHADOW_MODULE_PATTERNS) {
571
+ checks.push(check(`plan.rejects.${id}`, !pattern.test(prose), `the plan must not propose ${id.replaceAll('-', ' ')}`))
572
+ }
573
+
574
+ const generated = generatedReferenceGrounding(root)
575
+ checks.push(check(
576
+ 'plan.reference-facts-readable',
577
+ generated !== null,
578
+ `the target must ship a complete activated-reference projection at ${REFERENCE_FACTS}`,
579
+ ))
580
+ checks.push(check(
581
+ 'plan.reference-sheet-route',
582
+ prose.includes(REFERENCE_SHEET) && prose.includes(REFERENCE_FACTS),
583
+ `the plan must name both ${REFERENCE_SHEET} and its machine-readable ${REFERENCE_FACTS} bundle`,
584
+ ))
585
+ if (generated !== null) {
586
+ checks.push(exactIdentityChecks(
587
+ prose,
588
+ [BULK_ACTION_CAPABILITY, BULK_PROGRESS_CAPABILITY, WORKFLOW_CAPABILITY],
589
+ 'plan.required-capability-identities',
590
+ 'the plan must name the exact bulk-action, progress, and workflow capability IDs from the canonical inventory',
591
+ ))
592
+ checks.push(exactIdentityChecks(
593
+ prose,
594
+ [BULK_CONTRIBUTION, ...generated.activationIds],
595
+ 'plan.contribution-activation-identities',
596
+ 'the plan must name the bulk contribution and every activation ID that binds it',
597
+ ))
598
+ checks.push(exactIdentityChecks(
599
+ prose,
600
+ generated.overrideIds,
601
+ 'plan.override-target-identities',
602
+ 'the plan must name the exact generated override-target IDs for the bulk contribution and its workers',
603
+ ))
604
+ checks.push(exactIdentityChecks(
605
+ prose,
606
+ [WORKFLOW_CONTRIBUTION, generated.specialistRoute, generated.workflowSource],
607
+ 'plan.specialist-identities',
608
+ 'the plan must name the exact workflow specialist contribution, specialist route, and source',
609
+ ))
610
+ checks.push(exactIdentityChecks(
611
+ prose,
612
+ [...generated.workerIds, 'progressJobId'],
613
+ 'plan.bulk-progress-identities',
614
+ 'the plan must name both durable workers and their connected progressJobId lifecycle',
615
+ ))
616
+ }
617
+
618
+ const sourceReferences = sourceReferenceGrounding(root)
619
+ checks.push(check(
620
+ 'plan.source-reference-inventory-readable',
621
+ sourceReferences !== null,
622
+ `the target must expose every required readable source reference in ${SOURCE_LINK_INVENTORY}`,
623
+ ))
624
+ if (sourceReferences !== null) {
625
+ checks.push(exactIdentityChecks(
626
+ prose,
627
+ [...REQUIRED_SOURCE_REFERENCES, ...REQUIRED_DESIGN_REFERENCES],
628
+ 'plan.source-reference-identities',
629
+ 'the plan must name every exact canonical, gallery, implementation, Figma, and token source-reference ID',
630
+ ))
631
+ checks.push(exactIdentityChecks(
632
+ prose,
633
+ [GALLERY_ITEM_ID, GALLERY_FAMILY_ID, GALLERY_ENTRY_ID],
634
+ 'plan.gallery-identities',
635
+ 'the UI plan must name the exact gallery item, family, and entry IDs',
636
+ ))
637
+ }
638
+
639
+ const designInventoryPath = grounding.inventory?.designSystemInventoryPath
640
+ const designGallery = grounding.inventory?.designSystemGallery
641
+ const designFoundation = grounding.inventory?.designFoundation
642
+ checks.push(check(
643
+ 'plan.design-inventory-projection',
644
+ designInventoryPath === '.ai/harness/design-system-inventory.json'
645
+ && designGallery !== null && typeof designGallery === 'object'
646
+ && designFoundation !== null && typeof designFoundation === 'object',
647
+ 'the canonical surface inventory must project its generated gallery and design-foundation facts',
648
+ ))
649
+ checks.push(check(
650
+ 'plan.ui-design-route',
651
+ prose.includes('om-backend-ui-design')
652
+ && prose.includes('/backend/design-system')
653
+ && /source-only/i.test(prose)
654
+ && /explicit activation/i.test(prose),
655
+ 'the UI plan must route through om-backend-ui-design and describe the gallery route as source-only until explicit activation',
656
+ ))
657
+ checks.push(exactIdentityChecks(
658
+ prose,
659
+ [GALLERY_PACKAGE_SHA, FOUNDATION_AUDITED_SHA, FOUNDATION_PACKAGE_SHA, 'publication not-evidenced'],
660
+ 'plan.design-provenance',
661
+ 'the plan must preserve the gallery and foundation provenance plus the non-publication posture',
662
+ ))
663
+ for (const { id, pattern } of UNSAFE_DESIGN_AUTHORITY_PATTERNS) {
664
+ checks.push(check(`plan.rejects.${id}`, !pattern.test(prose), `the plan must not claim ${id.replaceAll('-', ' ')}`))
665
+ }
666
+
667
+ const sections = level2Sections(markdown)
668
+ const traceability = sections.find((section) => TRACEABILITY_HEADING_PATTERN.test(section.heading))
669
+ if (!traceability) {
670
+ checks.push(check('plan.traceability.section', false, 'the plan needs a requirement traceability section'))
671
+ return checks
672
+ }
673
+ const rows = tableRows(traceability.body)
674
+ checks.push(check(
675
+ 'plan.traceability.rows',
676
+ rows.length >= contract.minTraceabilityRows,
677
+ `the traceability table needs at least ${contract.minTraceabilityRows} rows, one per added extension surface (found ${rows.length})`,
678
+ ))
679
+ const incomplete = rows.filter((row) => !(
680
+ REQUIREMENT_ID_PATTERN.test(row.text)
681
+ && PHASE_REFERENCE_PATTERN.test(row.text)
682
+ && TEST_IDENTIFIER_PATTERN.test(row.text)
683
+ ))
684
+ checks.push(check(
685
+ 'plan.traceability.complete',
686
+ rows.length > 0 && incomplete.length === 0,
687
+ `every traceability row must carry a requirement ID, an implementation phase, and a self-contained integration test (incomplete: ${incomplete.length})`,
688
+ ))
689
+ const classified = rows.map((row) => ({
690
+ row,
691
+ values: MECHANISM_CLASSIFICATIONS.filter((value) => row.text.includes(value)),
692
+ }))
693
+ checks.push(check(
694
+ 'plan.traceability.classification',
695
+ rows.length > 0 && classified.every((entry) => entry.values.length === 1),
696
+ `every traceability row must state exactly one mechanism classification from ${MECHANISM_CLASSIFICATIONS.join(', ')}`,
697
+ ))
698
+ checks.push(check(
699
+ 'plan.traceability.no-negative-fixture',
700
+ classified.every((entry) => !entry.values.includes(FORBIDDEN_CLASSIFICATION)),
701
+ `no proposed surface may be classified ${FORBIDDEN_CLASSIFICATION}: only a deliberately broken fixture produces it`,
702
+ ))
703
+ const requirementNumbers = rows.map((row) => Number(/\bREQ-(\d{3})\b/.exec(row.text)?.[1]))
704
+ const phaseNumbers = rows.map((row) => Number(/\bPhase\s+(\d+)\b/i.exec(row.text)?.[1]))
705
+ checks.push(check(
706
+ 'plan.traceability.ordering',
707
+ rows.length > 0
708
+ && requirementNumbers.every((value, index) => value === index + 1)
709
+ && phaseNumbers.every((value, index) => Number.isInteger(value) && (index === 0 || value >= phaseNumbers[index - 1])),
710
+ 'traceability rows must use contiguous requirement IDs and nondecreasing dependency-phase order',
711
+ ))
712
+ if (generated !== null && sourceReferences !== null) {
713
+ const expectedRows = [
714
+ {
715
+ id: 'canonical-data-command',
716
+ values: [
717
+ 'data.entities',
718
+ 'commands.write',
719
+ REQUIRED_SOURCE_REFERENCES[0],
720
+ REQUIRED_SOURCE_REFERENCES[1],
721
+ 'emitted-example',
722
+ ],
723
+ },
724
+ {
725
+ id: 'workflow-specialist',
726
+ values: [WORKFLOW_CAPABILITY, WORKFLOW_CONTRIBUTION, generated.specialistRoute, 'emitted-example'],
727
+ },
728
+ {
729
+ id: 'bulk-action',
730
+ values: [
731
+ BULK_ACTION_CAPABILITY,
732
+ BULK_CONTRIBUTION,
733
+ ...generated.activationIds,
734
+ REQUIRED_SOURCE_REFERENCES[2],
735
+ 'emitted-example',
736
+ ],
737
+ },
738
+ {
739
+ id: 'bulk-progress',
740
+ values: [BULK_PROGRESS_CAPABILITY, ...generated.workerIds, ...generated.overrideIds, 'progressJobId', 'emitted-example'],
741
+ },
742
+ {
743
+ id: 'ui-foundation',
744
+ values: [...REQUIRED_DESIGN_REFERENCES, GALLERY_ITEM_ID, GALLERY_PACKAGE_SHA, FOUNDATION_PACKAGE_SHA, 'framework-only'],
745
+ },
746
+ ]
747
+ for (const expected of expectedRows) {
748
+ checks.push(check(
749
+ `plan.traceability.binding.${expected.id}`,
750
+ rows.some((row) => expected.values.every((value) => row.text.includes(value))),
751
+ `one traceability row must bind ${expected.id.replaceAll('-', ' ')} identities, phase, test, and mechanism together`,
752
+ ))
753
+ }
754
+ }
755
+ return checks
756
+ }
757
+
758
+ function newFeatureChecks(root, definition) {
759
+ const authored = authoredSpecFiles(root)
760
+ const checks = [check(
761
+ 'spec.single',
762
+ authored.length === 1,
763
+ `exactly one new specification must exist under ${SPEC_ROOT} (found ${authored.length ? authored.join(', ') : 'none'})`,
764
+ )]
765
+ if (authored.length !== 1) return checks
766
+ const [name] = authored
767
+ checks.push(check('spec.named', SPEC_FILE_PATTERN.test(name), `the specification must be named {YYYY-MM-DD}-{slug}.md (found ${name})`))
768
+ const markdown = readContainedFile(root, `${SPEC_ROOT}/${name}`)
769
+ if (markdown === null) {
770
+ checks.push(check('spec.readable', false, `${SPEC_ROOT}/${name} must be a readable bounded regular file`))
771
+ return checks
772
+ }
773
+ checks.push(...specStructureChecks(markdown))
774
+ if (definition.moduleShaped) checks.push(...moduleShapedChecks(root, markdown, definition.moduleShaped))
775
+ return checks
776
+ }
777
+
778
+ function existingSpecChecks(root, definition) {
779
+ const authored = authoredSpecFiles(root)
780
+ const expected = definition.coveringSpec.slice(`${SPEC_ROOT}/`.length)
781
+ const checks = [check(
782
+ 'spec.single',
783
+ authored.length === 1 && authored[0] === expected,
784
+ `${definition.coveringSpec} must be the only specification (found ${authored.length ? authored.join(', ') : 'none'})`,
785
+ )]
786
+ const markdown = readContainedFile(root, definition.coveringSpec)
787
+ if (markdown === null) {
788
+ checks.push(check('spec.readable', false, `${definition.coveringSpec} must be a readable bounded regular file`))
789
+ return checks
790
+ }
791
+ checks.push(...specStructureChecks(markdown))
792
+ const prose = strippedOfCodeBlocks(markdown)
793
+ for (const amendment of definition.amendments) {
794
+ checks.push(check(
795
+ `spec.amendment.${amendment.id}`,
796
+ amendment.all.every((pattern) => pattern.test(prose)),
797
+ amendment.requirement,
798
+ ))
799
+ }
800
+ for (const preserved of definition.preserved) {
801
+ checks.push(check(`spec.preserved.${preserved.id}`, preserved.pattern.test(prose), preserved.requirement))
802
+ }
803
+ const entries = changelogEntryCount(markdown)
804
+ checks.push(check(
805
+ 'spec.changelog.amended',
806
+ entries >= definition.minChangelogEntries,
807
+ `the covering spec changelog must record the amendment (needs at least ${definition.minChangelogEntries} entries, found ${entries})`,
808
+ ))
809
+ return checks
810
+ }
811
+
812
+ export function evaluateWritableSpecOracle({ root: requestedRoot, caseId, phase }) {
813
+ if (typeof requestedRoot !== 'string' || !path.isAbsolute(requestedRoot)) throw new Error('root must be absolute')
814
+ const definition = SPEC_CASES[caseId]
815
+ if (!definition) throw new Error(`unsupported writable spec case: ${caseId}`)
816
+ if (!['before', 'after'].includes(phase)) throw new Error('phase must be before or after')
817
+ const root = fs.realpathSync(requestedRoot)
818
+ const checks = [
819
+ ...(definition.kind === 'new-feature' ? newFeatureChecks(root, definition) : existingSpecChecks(root, definition)),
820
+ ...reservedSpecChecks(root),
821
+ ...implementationChecks(root, definition),
822
+ ]
823
+ const failures = checks.filter((entry) => !entry.passed).map((entry) => `${entry.id}: ${entry.requirement}`)
824
+ return { passed: failures.length === 0, failures, checks }
825
+ }
826
+
827
+ function usage() {
828
+ return `Grade one writable planning proof against its fixed specification contract.
829
+
830
+ Usage:
831
+ node .ai/harness/writable-spec-oracles.mjs --root /absolute/app --case ${Object.keys(SPEC_CASES).join('|')} --phase before|after [--json]`
832
+ }
833
+
834
+ function parseArgs(argv) {
835
+ const options = { root: undefined, caseId: undefined, phase: undefined, json: false, help: false }
836
+ for (let index = 0; index < argv.length; index += 1) {
837
+ const arg = argv[index]
838
+ const value = () => {
839
+ const next = argv[index + 1]
840
+ if (!next || next.startsWith('--')) throw new Error(`${arg} requires a value`)
841
+ index += 1
842
+ return next
843
+ }
844
+ if (arg === '--help' || arg === '-h') options.help = true
845
+ else if (arg === '--root') options.root = value()
846
+ else if (arg === '--case') options.caseId = value()
847
+ else if (arg === '--phase') options.phase = value()
848
+ else if (arg === '--json') options.json = true
849
+ else throw new Error(`unknown argument: ${arg}`)
850
+ }
851
+ return options
852
+ }
853
+
854
+ function emit(result, json) {
855
+ if (json) process.stdout.write(`${JSON.stringify(result)}\n`)
856
+ else process.stdout.write(`${result.passed ? 'PASS' : 'FAIL'}: ${result.failures.join('; ') || 'all specification checks passed'}\n`)
857
+ }
858
+
859
+ function main() {
860
+ let options
861
+ try { options = parseArgs(process.argv.slice(2)) } catch (error) {
862
+ process.stdout.write(`${JSON.stringify({ passed: false, failures: [error.message], checks: [] })}\n`)
863
+ return EXIT_INVALID
864
+ }
865
+ if (options.help) { process.stdout.write(`${usage()}\n`); return EXIT_PASS }
866
+ try {
867
+ const result = evaluateWritableSpecOracle(options)
868
+ emit(result, options.json)
869
+ return result.passed ? EXIT_PASS : EXIT_FAILURE
870
+ } catch (error) {
871
+ const message = error instanceof Error ? error.message : String(error)
872
+ emit({ passed: false, failures: [message], checks: [] }, options.json)
873
+ return EXIT_INVALID
874
+ }
875
+ }
876
+
877
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
878
+ process.exitCode = main()
879
+ }