@open-mercato/cli 0.6.7 → 0.6.8-canary.319.1.5b4be4bc52
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.
- package/.turbo/turbo-build.log +77 -4
- package/build.mjs +68 -11
- package/dist/agentic/claude-code/hooks/entity-migration-check.ts +32 -88
- package/dist/agentic/codex/enforcement-rules.md +2 -16
- package/dist/agentic/cursor/hooks/entity-migration-check.mjs +19 -50
- package/dist/agentic/cursor/rules/entity-guard.mdc +9 -21
- package/dist/agentic/cursor/rules/generated-guard.mdc +3 -5
- package/dist/agentic/cursor/rules/open-mercato.mdc +3 -29
- package/dist/agentic/guides/ai-workflows.md +76 -0
- package/dist/agentic/guides/architecture.md +107 -0
- package/dist/agentic/guides/backend-ui.md +84 -0
- package/dist/agentic/guides/contracts.md +92 -0
- package/dist/agentic/guides/extensions.md +121 -0
- package/dist/agentic/guides/framework-extension-points.md +36 -0
- package/dist/agentic/guides/integrations.md +81 -0
- package/dist/agentic/guides/module-facts.json +116757 -3951
- package/dist/agentic/guides/module-system.md +12 -127
- package/dist/agentic/guides/modules/ai_assistant.md +227 -28
- package/dist/agentic/guides/modules/api_docs.md +58 -2
- package/dist/agentic/guides/modules/api_keys.md +92 -7
- package/dist/agentic/guides/modules/attachments.md +119 -14
- package/dist/agentic/guides/modules/audit_logs.md +131 -10
- package/dist/agentic/guides/modules/auth.md +276 -33
- package/dist/agentic/guides/modules/business_rules.md +149 -14
- package/dist/agentic/guides/modules/catalog.md +482 -43
- package/dist/agentic/guides/modules/channel_gmail.md +82 -3
- package/dist/agentic/guides/modules/channel_imap.md +82 -3
- package/dist/agentic/guides/modules/checkout.md +305 -27
- package/dist/agentic/guides/modules/communication_channels.md +315 -33
- package/dist/agentic/guides/modules/configs.md +104 -8
- package/dist/agentic/guides/modules/content.md +57 -2
- package/dist/agentic/guides/modules/currencies.md +152 -17
- package/dist/agentic/guides/modules/customer_accounts.md +387 -45
- package/dist/agentic/guides/modules/customers.md +746 -85
- package/dist/agentic/guides/modules/dashboards.md +155 -10
- package/dist/agentic/guides/modules/data_sync.md +149 -16
- package/dist/agentic/guides/modules/design_system.md +102 -0
- package/dist/agentic/guides/modules/dictionaries.md +107 -12
- package/dist/agentic/guides/modules/directory.md +148 -15
- package/dist/agentic/guides/modules/entities.md +145 -13
- package/dist/agentic/guides/modules/events.md +59 -2
- package/dist/agentic/guides/modules/feature_toggles.md +119 -9
- package/dist/agentic/guides/modules/gateway_stripe.md +92 -4
- package/dist/agentic/guides/modules/generators.md +45 -2
- package/dist/agentic/guides/modules/inbox_ops.md +207 -27
- package/dist/agentic/guides/modules/integrations.md +149 -16
- package/dist/agentic/guides/modules/messages.md +223 -24
- package/dist/agentic/guides/modules/notifications.md +96 -8
- package/dist/agentic/guides/modules/onboarding.md +77 -6
- package/dist/agentic/guides/modules/payment_gateways.md +168 -21
- package/dist/agentic/guides/modules/perspectives.md +77 -7
- package/dist/agentic/guides/modules/planner.md +136 -18
- package/dist/agentic/guides/modules/portal.md +93 -2
- package/dist/agentic/guides/modules/progress.md +104 -14
- package/dist/agentic/guides/modules/query_index.md +154 -15
- package/dist/agentic/guides/modules/record_locks.md +206 -21
- package/dist/agentic/guides/modules/resources.md +197 -30
- package/dist/agentic/guides/modules/sales.md +687 -84
- package/dist/agentic/guides/modules/scheduler.md +133 -14
- package/dist/agentic/guides/modules/search.md +170 -6
- package/dist/agentic/guides/modules/security.md +294 -35
- package/dist/agentic/guides/modules/shipping_carriers.md +134 -18
- package/dist/agentic/guides/modules/sso.md +151 -26
- package/dist/agentic/guides/modules/staff.md +372 -57
- package/dist/agentic/guides/modules/storage_s3.md +95 -5
- package/dist/agentic/guides/modules/sync_akeneo.md +85 -3
- package/dist/agentic/guides/modules/sync_excel.md +75 -6
- package/dist/agentic/guides/modules/system_status_overlays.md +60 -2
- package/dist/agentic/guides/modules/translations.md +104 -10
- package/dist/agentic/guides/modules/webhooks.md +203 -24
- package/dist/agentic/guides/modules/wms.md +469 -47
- package/dist/agentic/guides/modules/workflows.md +302 -43
- package/dist/agentic/guides/reference-module-facts.json +6370 -0
- package/dist/agentic/guides/reference-modules/example.md +451 -0
- package/dist/agentic/guides/spec-delivery.md +42 -0
- package/dist/agentic/guides/testing-debugging.md +80 -0
- package/dist/agentic/guides/upstream/AGENTS.md +299 -0
- package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +316 -0
- package/dist/agentic/guides/upstream/manifest.json +8 -0
- package/dist/agentic/shared/AGENTS.md.template +120 -311
- package/dist/agentic/shared/ai/agentic.config.json +2 -0
- package/dist/agentic/shared/ai/harness/README.md +50 -0
- package/dist/agentic/shared/ai/harness/RELEASE.md +57 -0
- package/dist/agentic/shared/ai/harness/cases.json +15050 -0
- package/dist/agentic/shared/ai/harness/cases.schema.json +188 -0
- package/dist/agentic/shared/ai/harness/fixtures/index.json +51 -0
- package/dist/agentic/shared/ai/harness/fixtures/seeds.json +166 -0
- package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +21 -0
- package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +96 -0
- package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +172 -0
- package/dist/agentic/shared/ai/harness/knowledge-change.schema.json +228 -0
- package/dist/agentic/shared/ai/harness/release-matrix.json +163 -0
- package/dist/agentic/shared/ai/harness/release-result.schema.json +219 -0
- package/dist/agentic/shared/ai/harness/result.schema.json +109 -0
- package/dist/agentic/shared/ai/harness/routing-response.schema.json +22 -0
- package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +73 -0
- package/dist/agentic/shared/ai/harness/validators.json +82 -0
- package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +2121 -0
- package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +1096 -0
- package/dist/agentic/shared/ai/harness/writable-spec-oracles.mjs +412 -0
- package/dist/agentic/shared/ai/lessons/_template.md +16 -0
- package/dist/agentic/shared/ai/lessons.md +26 -3
- package/dist/agentic/shared/ai/review-checklist.md +36 -0
- package/dist/agentic/shared/ai/skills/om-auto-implement-spec/SKILL.md +21 -0
- package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +4 -0
- package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.6-to-0.6.7/SKILL.md +98 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +21 -241
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +31 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +53 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +15 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +11 -0
- package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +30 -0
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +12 -0
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +13 -0
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +14 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +28 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +12 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +22 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +12 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +19 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +17 -601
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +21 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +15 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +16 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +10 -0
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +15 -310
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +19 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +31 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +100 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +16 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/knowledge-change.md +115 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +17 -0
- package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +25 -0
- package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +11 -0
- package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +13 -0
- package/dist/agentic/shared/ai/skills/om-help/SKILL.md +15 -97
- package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +18 -0
- package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +20 -0
- package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +33 -152
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +33 -0
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/planning-and-progress.md +40 -0
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/report-templates.md +27 -0
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/spec-resolution.md +24 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +25 -708
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +16 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +25 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +15 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +12 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/SKILL.md +34 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/agents/openai.yaml +4 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/agentic-setup.md +11 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/input-normalization.md +31 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +39 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/report-template.md +43 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/rules.md +11 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +20 -820
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +32 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +172 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +14 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +83 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +40 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +11 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +22 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +13 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/SKILL.md +41 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/agentic-setup.md +19 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/bundle-preparation.md +44 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/consent-and-review.md +39 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/publication.md +20 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/report-templates.md +57 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/rules.md +11 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +486 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +20 -843
- package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +42 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +27 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +13 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +43 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +11 -0
- package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +15 -63
- package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +15 -0
- package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +17 -467
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +29 -0
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +14 -0
- package/dist/agentic/shared/ai/skills/tiers.json +143 -21
- package/dist/agentic/shared/ai/skills/tiers.schema.json +52 -40
- package/dist/agentic/shared/ai/specs/README.md +4 -3
- package/dist/agentic/shared/ai/specs/SPEC-000-template.md +226 -10
- package/dist/agentic/shared/ai/trackers/github.md +108 -4
- package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +224 -0
- package/dist/agentic/shared/scripts/check-lessons.mjs +178 -0
- package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +3585 -0
- package/dist/agentic/shared/scripts/execution-sandbox.mjs +329 -0
- package/dist/agentic/shared/scripts/framework-context.mjs +888 -0
- package/dist/agentic/shared/scripts/install-skills.mjs +1029 -0
- package/dist/agentic/shared/scripts/install-skills.sh +4 -694
- package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +222 -0
- package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +1774 -0
- package/dist/agentic/shared/scripts/validate-knowledge-change.mjs +911 -0
- package/dist/bin.js +13 -1
- package/dist/bin.js.map +2 -2
- package/dist/lib/__integration__/TC-INT-008.spec.js +88 -8
- package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
- package/dist/lib/agentic-init.js +13 -3
- package/dist/lib/agentic-init.js.map +2 -2
- package/dist/lib/agentic-setup.js +434 -186
- package/dist/lib/agentic-setup.js.map +2 -2
- package/dist/lib/generators/extensions/injection-widgets.js +20 -0
- package/dist/lib/generators/extensions/injection-widgets.js.map +2 -2
- package/dist/lib/generators/module-extension-facts.js +1872 -0
- package/dist/lib/generators/module-extension-facts.js.map +7 -0
- package/dist/lib/generators/module-fact-sources.js +70 -0
- package/dist/lib/generators/module-fact-sources.js.map +7 -0
- package/dist/lib/generators/module-facts-discovery.js +64 -7
- package/dist/lib/generators/module-facts-discovery.js.map +2 -2
- package/dist/lib/generators/module-facts.js +1629 -52
- package/dist/lib/generators/module-facts.js.map +3 -3
- package/dist/lib/generators/module-override-targets.js +688 -0
- package/dist/lib/generators/module-override-targets.js.map +7 -0
- package/dist/lib/generators/module-registry.js +11 -5
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/load-env.js +24 -0
- package/dist/lib/load-env.js.map +7 -0
- package/dist/lib/resolver.js +2 -0
- package/dist/lib/resolver.js.map +2 -2
- package/dist/lib/telemetry-init.js +358 -0
- package/dist/lib/telemetry-init.js.map +7 -0
- package/dist/lib/testing/integration.js +5 -2
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/mercato.js +23 -1
- package/dist/mercato.js.map +2 -2
- package/package.json +13 -11
- package/src/bin.ts +33 -1
- package/src/lib/__integration__/TC-INT-008.spec.ts +107 -9
- package/src/lib/__tests__/agentic-init.test.ts +41 -5
- package/src/lib/__tests__/agentic-setup.ownership.test.ts +386 -0
- package/src/lib/__tests__/injection-widget-id-validation.test.ts +184 -0
- package/src/lib/__tests__/load-env.test.ts +65 -0
- package/src/lib/__tests__/telemetry-init.test.ts +298 -0
- package/src/lib/agentic-init.ts +15 -3
- package/src/lib/agentic-setup.ts +573 -222
- package/src/lib/generators/__tests__/activation-adapter-coverage.test.ts +92 -0
- package/src/lib/generators/__tests__/module-extension-activations.test.ts +762 -0
- package/src/lib/generators/__tests__/module-extension-facts.test.ts +603 -0
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +139 -2
- package/src/lib/generators/__tests__/module-facts.computed-di-token.test.ts +88 -0
- package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +1 -0
- package/src/lib/generators/__tests__/module-facts.discovery.test.ts +54 -10
- package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +108 -0
- package/src/lib/generators/__tests__/module-facts.local-reference.test.ts +391 -0
- package/src/lib/generators/__tests__/module-facts.owned-contracts.test.ts +561 -0
- package/src/lib/generators/__tests__/module-facts.search-entity-id.test.ts +151 -0
- package/src/lib/generators/__tests__/module-facts.source-links.test.ts +160 -0
- package/src/lib/generators/__tests__/module-override-targets.roundtrip.test.ts +149 -0
- package/src/lib/generators/__tests__/module-override-targets.test.ts +328 -0
- package/src/lib/generators/__tests__/module-subset.test.ts +18 -0
- package/src/lib/generators/extensions/injection-widgets.ts +51 -0
- package/src/lib/generators/module-extension-facts.ts +2433 -0
- package/src/lib/generators/module-fact-sources.ts +108 -0
- package/src/lib/generators/module-facts-discovery.ts +104 -7
- package/src/lib/generators/module-facts.ts +2387 -138
- package/src/lib/generators/module-override-targets.ts +863 -0
- package/src/lib/generators/module-registry.ts +16 -6
- package/src/lib/load-env.ts +30 -0
- package/src/lib/resolver.ts +3 -0
- package/src/lib/telemetry-init.ts +461 -0
- package/src/lib/testing/__tests__/integration.test.ts +4 -0
- package/src/lib/testing/integration.ts +5 -2
- package/src/mercato.ts +26 -1
- package/dist/agentic/guides/cache.md +0 -45
- package/dist/agentic/guides/core.md +0 -453
- package/dist/agentic/guides/events.md +0 -117
- package/dist/agentic/guides/queue.md +0 -53
- package/dist/agentic/guides/search.md +0 -115
- package/dist/agentic/guides/shared.md +0 -108
- package/dist/agentic/guides/ui.md +0 -347
- package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +0 -27
- package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +0 -27
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +0 -260
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +0 -146
- package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +0 -84
- package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +0 -193
- package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +0 -34
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +0 -762
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +0 -50
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +0 -97
- package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +0 -271
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +0 -101
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import { createHash } from 'node:crypto'
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import os from 'node:os'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
import process from 'node:process'
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
10
|
+
|
|
11
|
+
const EXIT_PASS = 0
|
|
12
|
+
const EXIT_FAILURE = 1
|
|
13
|
+
const EXIT_INVALID = 2
|
|
14
|
+
|
|
15
|
+
export const CHANGE_CLASSES = Object.freeze(['knowledge-contract', 'asset-sync'])
|
|
16
|
+
export const CHANGED_CONTRACTS = Object.freeze([
|
|
17
|
+
'routing', 'skill-link', 'source-link', 'example-source', 'installed-source',
|
|
18
|
+
'discovery', 'context-read', 'evaluator', 'oracle',
|
|
19
|
+
])
|
|
20
|
+
export const RELEASE_LANE_IDS = Object.freeze([
|
|
21
|
+
'deterministic', 'routing', 'writable', 'generatedCodeReview', 'generativeJudge', 'generatedTests',
|
|
22
|
+
])
|
|
23
|
+
export const CONTROLLER_OWNED_FIELDS = Object.freeze(['resolvedBaseSha', 'headSha', 'focusedExecutions'])
|
|
24
|
+
export const SCHEMA_BASENAME = 'knowledge-change.schema.json'
|
|
25
|
+
|
|
26
|
+
// Sibling of the baseline and topics registry, which live OUTSIDE the shipped
|
|
27
|
+
// `agentic/shared/**` tree on purpose: that tree is copied wholesale into every generated
|
|
28
|
+
// app, and these are monorepo-only assets (the baseline pins monorepo SHAs and validates
|
|
29
|
+
// monorepo files). A forward reference to the old location would have sent the inventory
|
|
30
|
+
// back into every scaffold when it lands.
|
|
31
|
+
export const SOURCE_LINK_INVENTORY_PATH =
|
|
32
|
+
'packages/create-app/scripts/source-links/source-link-inventory.json'
|
|
33
|
+
export const CANON_C_REASON =
|
|
34
|
+
`${SOURCE_LINK_INVENTORY_PATH} not present — CANON-C (canonical-example source-link inventory) is a ` +
|
|
35
|
+
'monorepo-only asset and is absent from this tree, so source-link, example-source, and ' +
|
|
36
|
+
'installed-source contracts cannot be resolved and fail closed'
|
|
37
|
+
|
|
38
|
+
const EXAMPLE_AUTHORING_ROOT = 'apps/mercato/src/modules/example'
|
|
39
|
+
const EXAMPLE_TEMPLATE_ROOT = 'packages/create-app/template/src/modules/example'
|
|
40
|
+
const EXAMPLE_QA_ONLY_SEGMENTS = Object.freeze(['__tests__', '__integration__'])
|
|
41
|
+
|
|
42
|
+
const FOCUSED_TEST_PATTERN = /\.(?:test|spec)\.[cm]?[jt]sx?$/
|
|
43
|
+
|
|
44
|
+
// One argv token may not carry a shell metacharacter. The controller never interpolates a shell,
|
|
45
|
+
// and the recorded evidence must stay reproducible as an exact argv vector.
|
|
46
|
+
const SHELL_METACHARACTER_PATTERN = /[`$;&|<>\n]/
|
|
47
|
+
|
|
48
|
+
export const DEFAULT_EXECUTION_TIMEOUT_MS = 600_000
|
|
49
|
+
|
|
50
|
+
// Ordered; the first matching runner owns the focused test. Runner argv is controller-derived so an
|
|
51
|
+
// author can never substitute a weaker command for the test they declared.
|
|
52
|
+
const FOCUSED_RUNNERS = Object.freeze([
|
|
53
|
+
{ runner: 'node-test', pattern: /\.(?:test|spec)\.[cm]?jsx?$/, args: (relativePath) => ['--test', relativePath] },
|
|
54
|
+
{ runner: 'node-test-tsx', pattern: /\.(?:test|spec)\.[cm]?tsx?$/, args: (relativePath) => ['--import', 'tsx', '--test', relativePath] },
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
// Ordered; the first matching rule owns the path. `contract: null` marks a path that
|
|
58
|
+
// carries no knowledge contract of its own — a generated/materialized copy or a docs
|
|
59
|
+
// snapshot — which is the only shape an `asset-sync` run may contain.
|
|
60
|
+
const CLASSIFICATION_RULES = Object.freeze([
|
|
61
|
+
{
|
|
62
|
+
rule: 'canonical-example-tree',
|
|
63
|
+
contract: 'example-source',
|
|
64
|
+
changeClass: 'knowledge-contract',
|
|
65
|
+
pattern: /^(?:apps\/mercato\/src|packages\/create-app\/template\/src|src)\/modules\/example\/.+$/,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
rule: 'source-link-inventory',
|
|
69
|
+
contract: 'source-link',
|
|
70
|
+
changeClass: 'knowledge-contract',
|
|
71
|
+
pattern: /(?:^|\/)(?:source-link-inventory\.json|source-link-baseline\.json|source-link-baseline\.schema\.json|source-link-parity-ledger\.json)$/,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
rule: 'installed-package-manifest',
|
|
75
|
+
contract: 'installed-source',
|
|
76
|
+
changeClass: 'knowledge-contract',
|
|
77
|
+
pattern: /(?:^|\/)(?:package\.json|package\.json\.template|yarn\.lock|package-lock\.json|pnpm-lock\.yaml)$/,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
rule: 'installed-skill-closure',
|
|
81
|
+
contract: 'installed-source',
|
|
82
|
+
changeClass: 'knowledge-contract',
|
|
83
|
+
pattern: /(?:^|\/)skills\/tiers\.json$/,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
rule: 'evaluator-implementation',
|
|
87
|
+
contract: 'evaluator',
|
|
88
|
+
changeClass: 'knowledge-contract',
|
|
89
|
+
pattern: /(?:^|\/)scripts\/(?:evaluate-agent-harness|agent-harness-tool-server|run-agent-harness-release|prepare-agent-harness-fixture|execution-sandbox|framework-context|validate-knowledge-change)\.mjs$/,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
rule: 'writable-oracle',
|
|
93
|
+
contract: 'oracle',
|
|
94
|
+
changeClass: 'knowledge-contract',
|
|
95
|
+
pattern: /(?:^|\/)harness\/(?:writable-ast-oracles\.mjs|writable-behavior-oracles\.mjs|validators\.json)$/,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
rule: 'case-context-policy',
|
|
99
|
+
contract: 'context-read',
|
|
100
|
+
changeClass: 'knowledge-contract',
|
|
101
|
+
pattern: /(?:^|\/)harness\/(?:cases\.json|release-matrix\.json|fixtures\/.+|[^/]+\.schema\.json)$/,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
rule: 'agent-instruction-owner',
|
|
105
|
+
contract: 'routing',
|
|
106
|
+
changeClass: 'knowledge-contract',
|
|
107
|
+
pattern: /(?:^|\/)(?:AGENTS\.md|AGENTS\.md\.template|CLAUDE\.md|CLAUDE\.md\.template)$/,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
rule: 'generated-fact-copy',
|
|
111
|
+
contract: null,
|
|
112
|
+
changeClass: 'asset-sync',
|
|
113
|
+
pattern: /(?:^|\/)(?:\.ai|ai)\/guides\/(?:modules|upstream)\/.+$/,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
rule: 'routing-guide-owner',
|
|
117
|
+
contract: 'routing',
|
|
118
|
+
changeClass: 'knowledge-contract',
|
|
119
|
+
pattern: /(?:^|\/)(?:\.ai|ai)\/guides\/[^/]+\.md$/,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
rule: 'skill-authority',
|
|
123
|
+
contract: 'skill-link',
|
|
124
|
+
changeClass: 'knowledge-contract',
|
|
125
|
+
pattern: /(?:^|\/)skills\/(?:[^/]+\/(?:SKILL\.md|references\/.+|scripts\/.+)|tiers\.schema\.json)$/,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
rule: 'discovery-generator-contract',
|
|
129
|
+
contract: 'discovery',
|
|
130
|
+
changeClass: 'knowledge-contract',
|
|
131
|
+
pattern: /^(?:packages\/cli\/src\/lib\/generators\/.+|packages\/create-app\/src\/setup\/.+|packages\/create-app\/build\.mjs)$/,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
rule: 'materialized-copy',
|
|
135
|
+
contract: null,
|
|
136
|
+
changeClass: 'asset-sync',
|
|
137
|
+
pattern: /^(?:packages\/create-app\/dist\/.+|apps\/docs\/.+|\.ai\/specs\/.+\.md)$/,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
rule: 'docs-count-snapshot',
|
|
141
|
+
contract: null,
|
|
142
|
+
changeClass: 'asset-sync',
|
|
143
|
+
pattern: /(?:^|\/)(?:README\.md|RELEASE\.md|CHANGELOG\.md)$/,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
rule: 'focused-test',
|
|
147
|
+
contract: 'evaluator',
|
|
148
|
+
changeClass: 'knowledge-contract',
|
|
149
|
+
pattern: FOCUSED_TEST_PATTERN,
|
|
150
|
+
},
|
|
151
|
+
])
|
|
152
|
+
|
|
153
|
+
export function normalizeRelativePath(value) {
|
|
154
|
+
return String(value ?? '').split('\\').join('/').replace(/^\.\//, '')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function isFocusedTestPath(value) {
|
|
158
|
+
return FOCUSED_TEST_PATTERN.test(normalizeRelativePath(value))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function classifyChangedPath(value) {
|
|
162
|
+
const relativePath = normalizeRelativePath(value)
|
|
163
|
+
for (const rule of CLASSIFICATION_RULES) {
|
|
164
|
+
if (!rule.pattern.test(relativePath)) continue
|
|
165
|
+
return {
|
|
166
|
+
path: relativePath,
|
|
167
|
+
rule: rule.rule,
|
|
168
|
+
contract: rule.contract,
|
|
169
|
+
changeClass: rule.changeClass,
|
|
170
|
+
focusedTest: isFocusedTestPath(relativePath),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
path: relativePath,
|
|
175
|
+
rule: 'unclassified-fails-closed',
|
|
176
|
+
contract: null,
|
|
177
|
+
changeClass: 'knowledge-contract',
|
|
178
|
+
focusedTest: false,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function deriveChangeClassification(paths) {
|
|
183
|
+
const records = [...new Set((paths ?? []).map(normalizeRelativePath))].sort().map(classifyChangedPath)
|
|
184
|
+
const contracts = new Set()
|
|
185
|
+
let changeClass = 'asset-sync'
|
|
186
|
+
for (const record of records) {
|
|
187
|
+
if (record.contract) contracts.add(record.contract)
|
|
188
|
+
if (record.changeClass === 'knowledge-contract') changeClass = 'knowledge-contract'
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
changeClass,
|
|
192
|
+
changedContracts: CHANGED_CONTRACTS.filter((contract) => contracts.has(contract)),
|
|
193
|
+
records,
|
|
194
|
+
unclassifiedPaths: records.filter((record) => record.rule === 'unclassified-fails-closed').map((record) => record.path),
|
|
195
|
+
focusedTestPaths: records.filter((record) => record.focusedTest).map((record) => record.path),
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function resolveSchemaReference(rootSchema, reference) {
|
|
200
|
+
if (typeof reference !== 'string' || !reference.startsWith('#/')) return null
|
|
201
|
+
let node = rootSchema
|
|
202
|
+
for (const rawSegment of reference.slice(2).split('/')) {
|
|
203
|
+
const segment = rawSegment.replaceAll('~1', '/').replaceAll('~0', '~')
|
|
204
|
+
if (node === null || typeof node !== 'object' || !Object.hasOwn(node, segment)) return null
|
|
205
|
+
node = node[segment]
|
|
206
|
+
}
|
|
207
|
+
return node
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isPlainObject(value) {
|
|
211
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function matchesSchemaType(value, type) {
|
|
215
|
+
if (type === 'object') return isPlainObject(value)
|
|
216
|
+
if (type === 'array') return Array.isArray(value)
|
|
217
|
+
if (type === 'string') return typeof value === 'string'
|
|
218
|
+
if (type === 'integer') return Number.isInteger(value)
|
|
219
|
+
if (type === 'number') return typeof value === 'number' && Number.isFinite(value)
|
|
220
|
+
if (type === 'boolean') return typeof value === 'boolean'
|
|
221
|
+
if (type === 'null') return value === null
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function validateJsonSchema(value, schema, location = '$', rootSchema = schema) {
|
|
226
|
+
if (schema === true) return []
|
|
227
|
+
if (schema === false) return [`${location} is not allowed by its schema`]
|
|
228
|
+
const errors = []
|
|
229
|
+
if (schema.$ref) {
|
|
230
|
+
const resolved = resolveSchemaReference(rootSchema, schema.$ref)
|
|
231
|
+
if (!resolved) return [`${location} has an unresolved schema reference ${schema.$ref}`]
|
|
232
|
+
errors.push(...validateJsonSchema(value, resolved, location, rootSchema))
|
|
233
|
+
}
|
|
234
|
+
for (const branch of schema.allOf ?? []) errors.push(...validateJsonSchema(value, branch, location, rootSchema))
|
|
235
|
+
if (Array.isArray(schema.anyOf) && !schema.anyOf.some((branch) => validateJsonSchema(value, branch, location, rootSchema).length === 0)) {
|
|
236
|
+
errors.push(`${location} does not match any allowed schema branch`)
|
|
237
|
+
}
|
|
238
|
+
if (Array.isArray(schema.oneOf)) {
|
|
239
|
+
const matched = schema.oneOf.filter((branch) => validateJsonSchema(value, branch, location, rootSchema).length === 0)
|
|
240
|
+
if (matched.length !== 1) errors.push(`${location} must match exactly one schema branch`)
|
|
241
|
+
}
|
|
242
|
+
if (Object.hasOwn(schema, 'not') && validateJsonSchema(value, schema.not, location, rootSchema).length === 0) {
|
|
243
|
+
errors.push(`${location} matches a forbidden schema branch${schema.description ? `: ${schema.description}` : ''}`)
|
|
244
|
+
}
|
|
245
|
+
const expectedTypes = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : []
|
|
246
|
+
if (expectedTypes.length && !expectedTypes.some((type) => matchesSchemaType(value, type))) {
|
|
247
|
+
errors.push(`${location} must be ${expectedTypes.join(' or ')}`)
|
|
248
|
+
return errors
|
|
249
|
+
}
|
|
250
|
+
if (Object.hasOwn(schema, 'const') && value !== schema.const) errors.push(`${location} must equal its schema constant`)
|
|
251
|
+
if (schema.enum && !schema.enum.some((item) => JSON.stringify(item) === JSON.stringify(value))) {
|
|
252
|
+
errors.push(`${location} is not in its schema enum`)
|
|
253
|
+
}
|
|
254
|
+
if (typeof value === 'string') {
|
|
255
|
+
if (Number.isInteger(schema.minLength) && value.length < schema.minLength) errors.push(`${location} is below minLength ${schema.minLength}`)
|
|
256
|
+
if (Number.isInteger(schema.maxLength) && value.length > schema.maxLength) errors.push(`${location} exceeds maxLength ${schema.maxLength}`)
|
|
257
|
+
if (schema.pattern && !new RegExp(schema.pattern).test(value)) errors.push(`${location} does not match its schema pattern`)
|
|
258
|
+
}
|
|
259
|
+
if (typeof value === 'number') {
|
|
260
|
+
if (Number.isFinite(schema.minimum) && value < schema.minimum) errors.push(`${location} is below minimum ${schema.minimum}`)
|
|
261
|
+
if (Number.isFinite(schema.maximum) && value > schema.maximum) errors.push(`${location} exceeds maximum ${schema.maximum}`)
|
|
262
|
+
}
|
|
263
|
+
if (Array.isArray(value)) {
|
|
264
|
+
if (Number.isInteger(schema.minItems) && value.length < schema.minItems) errors.push(`${location} is below minItems ${schema.minItems}`)
|
|
265
|
+
if (Number.isInteger(schema.maxItems) && value.length > schema.maxItems) errors.push(`${location} exceeds maxItems ${schema.maxItems}`)
|
|
266
|
+
if (schema.uniqueItems && new Set(value.map((item) => JSON.stringify(item))).size !== value.length) errors.push(`${location} must contain unique items`)
|
|
267
|
+
if (schema.items) value.forEach((item, index) => errors.push(...validateJsonSchema(item, schema.items, `${location}[${index}]`, rootSchema)))
|
|
268
|
+
}
|
|
269
|
+
if (isPlainObject(value)) {
|
|
270
|
+
for (const required of schema.required ?? []) if (!Object.hasOwn(value, required)) errors.push(`${location}.${required} is required`)
|
|
271
|
+
if (schema.additionalProperties === false) {
|
|
272
|
+
for (const key of Object.keys(value)) if (!Object.hasOwn(schema.properties ?? {}, key)) errors.push(`${location}.${key} is not allowed`)
|
|
273
|
+
}
|
|
274
|
+
for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
|
|
275
|
+
if (Object.hasOwn(value, key)) errors.push(...validateJsonSchema(value[key], childSchema, `${location}.${key}`, rootSchema))
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return errors
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function hashFile(absolutePath) {
|
|
282
|
+
return createHash('sha256').update(fs.readFileSync(absolutePath)).digest('hex')
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isRegularFile(absolutePath) {
|
|
286
|
+
try { return fs.statSync(absolutePath).isFile() } catch { return false }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function readJsonIfPresent(absolutePath) {
|
|
290
|
+
if (!isRegularFile(absolutePath)) return null
|
|
291
|
+
try { return JSON.parse(fs.readFileSync(absolutePath, 'utf8')) } catch { return null }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function expandCaseRange(rangeId) {
|
|
295
|
+
const match = /^OMH-([0-9]{3})\.\.OMH-([0-9]{3})$/.exec(String(rangeId ?? ''))
|
|
296
|
+
if (!match) return null
|
|
297
|
+
const low = Number(match[1])
|
|
298
|
+
const high = Number(match[2])
|
|
299
|
+
if (low > high) return null
|
|
300
|
+
const ids = []
|
|
301
|
+
for (let index = low; index <= high; index += 1) ids.push(`OMH-${String(index).padStart(3, '0')}`)
|
|
302
|
+
return ids
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function git(root, args) {
|
|
306
|
+
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' })
|
|
307
|
+
if (result.status !== 0) {
|
|
308
|
+
throw new Error(`git ${args.join(' ')} failed: ${(result.stderr || result.stdout || '').trim()}`)
|
|
309
|
+
}
|
|
310
|
+
return result.stdout
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function resolveGitSha(root, ref) {
|
|
314
|
+
return git(root, ['rev-parse', '--verify', `${ref}^{commit}`]).trim()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function deriveChangedPaths(root, baseSha) {
|
|
318
|
+
const tracked = git(root, ['diff', '--name-only', '--no-renames', baseSha]).split('\n')
|
|
319
|
+
const untracked = git(root, ['ls-files', '--others', '--exclude-standard']).split('\n')
|
|
320
|
+
return [...new Set([...tracked, ...untracked].map((line) => line.trim()).filter(Boolean).map(normalizeRelativePath))].sort()
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function exampleMirrorPath(relativePath) {
|
|
324
|
+
if (relativePath.startsWith(`${EXAMPLE_AUTHORING_ROOT}/`)) {
|
|
325
|
+
return `${EXAMPLE_TEMPLATE_ROOT}/${relativePath.slice(EXAMPLE_AUTHORING_ROOT.length + 1)}`
|
|
326
|
+
}
|
|
327
|
+
if (relativePath.startsWith(`${EXAMPLE_TEMPLATE_ROOT}/`)) {
|
|
328
|
+
return `${EXAMPLE_AUTHORING_ROOT}/${relativePath.slice(EXAMPLE_TEMPLATE_ROOT.length + 1)}`
|
|
329
|
+
}
|
|
330
|
+
return null
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function exampleReadStatus(relativePath) {
|
|
334
|
+
const segments = normalizeRelativePath(relativePath).split('/')
|
|
335
|
+
return segments.some((segment) => EXAMPLE_QA_ONLY_SEGMENTS.includes(segment)) ? 'qa-only' : 'readable'
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function checkExampleSourceMirrors(root, records, errors, derived) {
|
|
339
|
+
const mirrors = []
|
|
340
|
+
for (const record of records) {
|
|
341
|
+
if (record.contract !== 'example-source') continue
|
|
342
|
+
const mirror = exampleMirrorPath(record.path)
|
|
343
|
+
if (!mirror) continue
|
|
344
|
+
const sourceAbsolute = path.join(root, record.path)
|
|
345
|
+
const mirrorAbsolute = path.join(root, mirror)
|
|
346
|
+
const sourceExists = isRegularFile(sourceAbsolute)
|
|
347
|
+
const mirrorExists = isRegularFile(mirrorAbsolute)
|
|
348
|
+
const entry = { path: record.path, mirrorPath: mirror, readStatus: exampleReadStatus(record.path) }
|
|
349
|
+
if (!sourceExists && !mirrorExists) {
|
|
350
|
+
entry.state = 'deleted-on-both-sides'
|
|
351
|
+
} else if (!sourceExists || !mirrorExists) {
|
|
352
|
+
entry.state = 'mirror-missing'
|
|
353
|
+
errors.push(
|
|
354
|
+
`example-source ${record.path} was moved or deleted without its byte-identical mirror ${mirror}; ` +
|
|
355
|
+
'run `yarn template:sync:fix` and update the surface inventory',
|
|
356
|
+
)
|
|
357
|
+
} else if (hashFile(sourceAbsolute) !== hashFile(mirrorAbsolute)) {
|
|
358
|
+
entry.state = 'mirror-drift'
|
|
359
|
+
errors.push(`example-source ${record.path} is not byte-identical to its mirror ${mirror}; run \`yarn template:sync:fix\``)
|
|
360
|
+
} else {
|
|
361
|
+
entry.state = 'mirrored'
|
|
362
|
+
entry.sha256 = hashFile(sourceAbsolute)
|
|
363
|
+
}
|
|
364
|
+
mirrors.push(entry)
|
|
365
|
+
}
|
|
366
|
+
derived.exampleSourceMirrors = mirrors
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function checkFileRecords(root, label, records, errors, options = {}) {
|
|
370
|
+
for (const record of records) {
|
|
371
|
+
const absolute = path.join(root, record.path)
|
|
372
|
+
if (!isRegularFile(absolute)) {
|
|
373
|
+
errors.push(`${label} ${record.path} is not an exact regular file in the working tree`)
|
|
374
|
+
continue
|
|
375
|
+
}
|
|
376
|
+
const actual = hashFile(absolute)
|
|
377
|
+
if (actual !== record.sha256) {
|
|
378
|
+
errors.push(`${label} ${record.path} sha256 is stale: manifest ${record.sha256}, working tree ${actual}`)
|
|
379
|
+
}
|
|
380
|
+
if (!options.sourcePathRequired) continue
|
|
381
|
+
const sourceAbsolute = path.join(root, record.sourcePath)
|
|
382
|
+
if (!isRegularFile(sourceAbsolute)) {
|
|
383
|
+
errors.push(`${label} ${record.path} names sourcePath ${record.sourcePath}, which is not an exact regular file`)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function baseFileHash(root, baseSha, relativePath) {
|
|
389
|
+
const result = spawnSync('git', ['-C', root, 'show', `${baseSha}:${relativePath}`], { encoding: 'buffer' })
|
|
390
|
+
if (result.status !== 0) return null
|
|
391
|
+
return createHash('sha256').update(result.stdout).digest('hex')
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function baseFileBuffer(root, baseSha, relativePath) {
|
|
395
|
+
const result = spawnSync('git', ['-C', root, 'show', `${baseSha}:${relativePath}`], { encoding: 'buffer' })
|
|
396
|
+
return result.status === 0 ? result.stdout : null
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const NODE_TEST_SCRIPT_PATTERN = /(?:^|\s)--test(?:[=\s]|$)/
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* The controller only drives an exact `node --test` argv. It reads the nearest owning package's
|
|
403
|
+
* declared test script so a project on another runner is refused with its real script name instead
|
|
404
|
+
* of being handed a command that would not run its suite.
|
|
405
|
+
*/
|
|
406
|
+
export function resolveTestRunnerFamily(root, testFile) {
|
|
407
|
+
const segments = normalizeRelativePath(testFile).split('/').slice(0, -1)
|
|
408
|
+
const directories = ['']
|
|
409
|
+
let accumulated = ''
|
|
410
|
+
for (const segment of segments) {
|
|
411
|
+
accumulated = accumulated ? `${accumulated}/${segment}` : segment
|
|
412
|
+
directories.push(accumulated)
|
|
413
|
+
}
|
|
414
|
+
for (const directory of directories.reverse()) {
|
|
415
|
+
const manifest = readJsonIfPresent(path.join(root, directory, 'package.json'))
|
|
416
|
+
const declared = manifest?.scripts?.test
|
|
417
|
+
if (typeof declared !== 'string' || !declared.trim()) continue
|
|
418
|
+
return NODE_TEST_SCRIPT_PATTERN.test(declared)
|
|
419
|
+
? { family: 'node-test', declaredScript: declared }
|
|
420
|
+
: { family: 'unsupported', declaredScript: declared }
|
|
421
|
+
}
|
|
422
|
+
return { family: 'node-test', declaredScript: null }
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function deriveFocusedCommand(testFile, runnerFamily = { family: 'node-test', declaredScript: null }) {
|
|
426
|
+
const relativePath = normalizeRelativePath(testFile)
|
|
427
|
+
if (runnerFamily.family !== 'node-test') {
|
|
428
|
+
return {
|
|
429
|
+
error:
|
|
430
|
+
`the owning package of ${relativePath} declares the test script "${runnerFamily.declaredScript}", which the ` +
|
|
431
|
+
'controller cannot drive as an exact argv; it only drives a `node --test` runner today',
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const runner = FOCUSED_RUNNERS.find((candidate) => candidate.pattern.test(relativePath))
|
|
435
|
+
if (!runner) return { error: `no controller runner is registered for ${relativePath}` }
|
|
436
|
+
const argv = [process.execPath, ...runner.args(relativePath)]
|
|
437
|
+
const offending = argv.find((token) => SHELL_METACHARACTER_PATTERN.test(token))
|
|
438
|
+
if (offending !== undefined) {
|
|
439
|
+
return { error: `the command derived for ${relativePath} carries the shell metacharacter token "${offending}"` }
|
|
440
|
+
}
|
|
441
|
+
return { runner: runner.runner, argv }
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function isContainedRelativePath(relativePath) {
|
|
445
|
+
const normalized = normalizeRelativePath(relativePath)
|
|
446
|
+
if (!normalized || normalized.startsWith('/') || /^[A-Za-z]:/.test(normalized)) return false
|
|
447
|
+
return !normalized.split('/').includes('..')
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function overlayWorkingTreePaths(root, worktree, relativePaths) {
|
|
451
|
+
for (const relativePath of relativePaths) {
|
|
452
|
+
if (!isContainedRelativePath(relativePath)) continue
|
|
453
|
+
const source = path.join(root, relativePath)
|
|
454
|
+
const target = path.join(worktree, relativePath)
|
|
455
|
+
if (isRegularFile(source)) {
|
|
456
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
457
|
+
fs.copyFileSync(source, target)
|
|
458
|
+
} else if (fs.existsSync(target)) {
|
|
459
|
+
fs.rmSync(target, { force: true, recursive: true })
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function linkResolutionRoots(root, worktree, relativePath) {
|
|
465
|
+
const segments = normalizeRelativePath(relativePath).split('/').slice(0, -1)
|
|
466
|
+
const directories = ['']
|
|
467
|
+
let accumulated = ''
|
|
468
|
+
for (const segment of segments) {
|
|
469
|
+
accumulated = accumulated ? `${accumulated}/${segment}` : segment
|
|
470
|
+
directories.push(accumulated)
|
|
471
|
+
}
|
|
472
|
+
for (const directory of directories) {
|
|
473
|
+
const target = path.join(root, directory, 'node_modules')
|
|
474
|
+
const link = path.join(worktree, directory, 'node_modules')
|
|
475
|
+
if (!fs.existsSync(target) || fs.existsSync(link)) continue
|
|
476
|
+
fs.mkdirSync(path.dirname(link), { recursive: true })
|
|
477
|
+
try { fs.symlinkSync(target, link, 'junction') } catch { /* resolution stays worktree-local */ }
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// A controller invoked from inside a test runner would otherwise leak its runner context into the
|
|
482
|
+
// child, which suppresses the child's own exit status and makes every focused run look like a pass.
|
|
483
|
+
export const STRIPPED_EXECUTION_ENV_KEYS = Object.freeze([
|
|
484
|
+
'NODE_OPTIONS', 'NODE_TEST_CONTEXT', 'NODE_V8_COVERAGE', 'TEST_RUNNER_CONCURRENCY',
|
|
485
|
+
])
|
|
486
|
+
|
|
487
|
+
export function sanitizeExecutionEnv(sourceEnv) {
|
|
488
|
+
const env = { ...sourceEnv }
|
|
489
|
+
for (const key of STRIPPED_EXECUTION_ENV_KEYS) delete env[key]
|
|
490
|
+
return env
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function runControlledCommand(cwd, argv, timeoutMs) {
|
|
494
|
+
const env = sanitizeExecutionEnv(process.env)
|
|
495
|
+
const result = spawnSync(argv[0], argv.slice(1), { cwd, env, timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 })
|
|
496
|
+
if (result.error || result.signal || typeof result.status !== 'number') {
|
|
497
|
+
return { failed: true, reason: result.signal ? `terminated by signal ${result.signal}` : String(result.error ?? 'no exit status') }
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
exitCode: result.status,
|
|
501
|
+
stdoutSha256: createHash('sha256').update(result.stdout ?? Buffer.alloc(0)).digest('hex'),
|
|
502
|
+
stderrSha256: createHash('sha256').update(result.stderr ?? Buffer.alloc(0)).digest('hex'),
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function addWorktree(root, sha, directory) {
|
|
507
|
+
git(root, ['worktree', 'add', '--detach', '--quiet', directory, sha])
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Returns true when the clean removal failed and left an administrative entry behind. The caller
|
|
511
|
+
// prunes only then: an unconditional prune would also drop sibling worktrees of the same repository
|
|
512
|
+
// whose directories happen to be unavailable, which is not this controller's to decide.
|
|
513
|
+
function removeWorktree(root, directory) {
|
|
514
|
+
try {
|
|
515
|
+
git(root, ['worktree', 'remove', '--force', directory])
|
|
516
|
+
return false
|
|
517
|
+
} catch {
|
|
518
|
+
fs.rmSync(directory, { recursive: true, force: true })
|
|
519
|
+
return true
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Controller-owned focused execution. For each declared focused test the controller builds two
|
|
525
|
+
* isolated worktrees: the base commit with ONLY that test's own head content applied, and the head
|
|
526
|
+
* commit with the whole working-tree diff applied. It runs the derived argv exactly once per side —
|
|
527
|
+
* there is no retry — and records exit codes plus output hashes as evidence.
|
|
528
|
+
*/
|
|
529
|
+
export function runFocusedExecutions(input) {
|
|
530
|
+
const root = path.resolve(input.root)
|
|
531
|
+
const baseSha = input.baseSha
|
|
532
|
+
const changedPaths = [...new Set((input.changedPaths ?? []).map(normalizeRelativePath))]
|
|
533
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_EXECUTION_TIMEOUT_MS
|
|
534
|
+
const executions = []
|
|
535
|
+
const errors = []
|
|
536
|
+
const testFiles = [...new Set((input.testFiles ?? []).map(normalizeRelativePath))]
|
|
537
|
+
if (!testFiles.length) return { executions, errors }
|
|
538
|
+
|
|
539
|
+
const scratch = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'om-knowledge-exec-'))
|
|
540
|
+
let removalFellBack = false
|
|
541
|
+
try {
|
|
542
|
+
for (const [index, testFile] of testFiles.entries()) {
|
|
543
|
+
const headAbsolute = path.join(root, testFile)
|
|
544
|
+
if (!isContainedRelativePath(testFile) || !isRegularFile(headAbsolute)) {
|
|
545
|
+
errors.push(`focusedExecutions ${testFile} is not an exact regular file inside the repository, so it cannot be executed`)
|
|
546
|
+
continue
|
|
547
|
+
}
|
|
548
|
+
const derivedCommand = deriveFocusedCommand(testFile, resolveTestRunnerFamily(root, testFile))
|
|
549
|
+
if (derivedCommand.error) { errors.push(`focusedExecutions ${derivedCommand.error}`); continue }
|
|
550
|
+
|
|
551
|
+
const headContents = fs.readFileSync(headAbsolute)
|
|
552
|
+
const baseContents = baseFileBuffer(root, baseSha, testFile)
|
|
553
|
+
if (baseContents !== null && baseContents.equals(headContents)) {
|
|
554
|
+
errors.push(
|
|
555
|
+
`focusedExecutions ${testFile} has no test-only diff against the base; a knowledge-contract change must add ` +
|
|
556
|
+
'or repair the regression that proves the new behavior',
|
|
557
|
+
)
|
|
558
|
+
continue
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const baseWorktree = path.join(scratch, `base-${index}`)
|
|
562
|
+
const headWorktree = path.join(scratch, `head-${index}`)
|
|
563
|
+
let baseOutcome
|
|
564
|
+
let headOutcome
|
|
565
|
+
try {
|
|
566
|
+
addWorktree(root, baseSha, baseWorktree)
|
|
567
|
+
overlayWorkingTreePaths(root, baseWorktree, [testFile])
|
|
568
|
+
linkResolutionRoots(root, baseWorktree, testFile)
|
|
569
|
+
baseOutcome = runControlledCommand(baseWorktree, derivedCommand.argv, timeoutMs)
|
|
570
|
+
|
|
571
|
+
addWorktree(root, input.headSha, headWorktree)
|
|
572
|
+
overlayWorkingTreePaths(root, headWorktree, changedPaths)
|
|
573
|
+
linkResolutionRoots(root, headWorktree, testFile)
|
|
574
|
+
headOutcome = runControlledCommand(headWorktree, derivedCommand.argv, timeoutMs)
|
|
575
|
+
} catch (error) {
|
|
576
|
+
errors.push(`focusedExecutions ${testFile} could not be executed in an isolated worktree: ${error.message}`)
|
|
577
|
+
continue
|
|
578
|
+
} finally {
|
|
579
|
+
removalFellBack = removeWorktree(root, baseWorktree) || removalFellBack
|
|
580
|
+
removalFellBack = removeWorktree(root, headWorktree) || removalFellBack
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (baseOutcome.failed || headOutcome.failed) {
|
|
584
|
+
const failing = baseOutcome.failed ? `base-plus-test-only (${baseOutcome.reason})` : `head (${headOutcome.reason})`
|
|
585
|
+
errors.push(`focusedExecutions ${testFile} produced no usable ${failing} exit status`)
|
|
586
|
+
continue
|
|
587
|
+
}
|
|
588
|
+
executions.push({
|
|
589
|
+
testFile,
|
|
590
|
+
command: derivedCommand.argv,
|
|
591
|
+
baseWithTestPatch: baseOutcome,
|
|
592
|
+
head: headOutcome,
|
|
593
|
+
})
|
|
594
|
+
}
|
|
595
|
+
} finally {
|
|
596
|
+
fs.rmSync(scratch, { recursive: true, force: true })
|
|
597
|
+
if (removalFellBack) { try { git(root, ['worktree', 'prune']) } catch { /* pruning is best effort */ } }
|
|
598
|
+
}
|
|
599
|
+
return { executions, errors }
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export function assertFocusedExecutionEvidence(declaredTests, evidence) {
|
|
603
|
+
const errors = []
|
|
604
|
+
if (!evidence) {
|
|
605
|
+
return ['the controller produced no focused base/head execution evidence; a knowledge-contract change cannot be certified without it']
|
|
606
|
+
}
|
|
607
|
+
errors.push(...(evidence.errors ?? []))
|
|
608
|
+
const executions = evidence.executions ?? []
|
|
609
|
+
for (const testFile of declaredTests) {
|
|
610
|
+
const execution = executions.find((entry) => entry.testFile === normalizeRelativePath(testFile))
|
|
611
|
+
if (!execution) {
|
|
612
|
+
errors.push(`focusedExecutions has no base/head execution record for declared focused test ${testFile}`)
|
|
613
|
+
continue
|
|
614
|
+
}
|
|
615
|
+
if (execution.baseWithTestPatch.exitCode === 0) {
|
|
616
|
+
errors.push(
|
|
617
|
+
`focusedExecutions ${testFile} exited 0 on base-plus-test-only; the focused test must fail for the old behavior`,
|
|
618
|
+
)
|
|
619
|
+
}
|
|
620
|
+
if (execution.head.exitCode !== 0) {
|
|
621
|
+
errors.push(`focusedExecutions ${testFile} exited ${execution.head.exitCode} at head; the focused test must pass after the change`)
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
for (const execution of executions) {
|
|
625
|
+
if (!declaredTests.map(normalizeRelativePath).includes(execution.testFile)) {
|
|
626
|
+
errors.push(`focusedExecutions carries an execution for ${execution.testFile}, which the manifest does not declare`)
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return errors
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Validates one knowledge-change run manifest against the derived classification of
|
|
634
|
+
* its own diff. `changedPaths` is supplied by the caller so the derivation stays
|
|
635
|
+
* testable without a shell; the CLI derives it from git.
|
|
636
|
+
*/
|
|
637
|
+
export function validateKnowledgeChange(input) {
|
|
638
|
+
const root = path.resolve(input.root)
|
|
639
|
+
const manifest = input.manifest
|
|
640
|
+
const schema = input.schema
|
|
641
|
+
const errors = []
|
|
642
|
+
const derived = {}
|
|
643
|
+
|
|
644
|
+
const schemaErrors = validateJsonSchema(manifest, schema.$defs.authoredManifest, '$', schema)
|
|
645
|
+
for (const field of CONTROLLER_OWNED_FIELDS) {
|
|
646
|
+
if (isPlainObject(manifest) && Object.hasOwn(manifest, field)) {
|
|
647
|
+
errors.push(`${field} is controller-owned output; author-supplied evidence fails validation`)
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
errors.push(...schemaErrors.map((error) => `manifest schema: ${error}`))
|
|
651
|
+
if (!isPlainObject(manifest)) return { ok: false, errors, derived }
|
|
652
|
+
|
|
653
|
+
const classification = deriveChangeClassification(input.changedPaths)
|
|
654
|
+
derived.changeClass = classification.changeClass
|
|
655
|
+
derived.changedContracts = classification.changedContracts
|
|
656
|
+
derived.classifiedPaths = classification.records
|
|
657
|
+
derived.unclassifiedPaths = classification.unclassifiedPaths
|
|
658
|
+
|
|
659
|
+
if (manifest.changeClass !== classification.changeClass) {
|
|
660
|
+
errors.push(
|
|
661
|
+
`declared changeClass "${manifest.changeClass}" differs from the class derived from the diff ` +
|
|
662
|
+
`"${classification.changeClass}"`,
|
|
663
|
+
)
|
|
664
|
+
}
|
|
665
|
+
const declaredContracts = Array.isArray(manifest.changedContracts) ? manifest.changedContracts : []
|
|
666
|
+
const missingContracts = classification.changedContracts.filter((contract) => !declaredContracts.includes(contract))
|
|
667
|
+
const extraContracts = declaredContracts.filter((contract) => !classification.changedContracts.includes(contract))
|
|
668
|
+
if (missingContracts.length) errors.push(`changedContracts omits derived contracts: ${missingContracts.join(', ')}`)
|
|
669
|
+
if (extraContracts.length) errors.push(`changedContracts declares contracts the diff does not touch: ${extraContracts.join(', ')}`)
|
|
670
|
+
|
|
671
|
+
checkFileRecords(root, 'authoritativeFiles', manifest.authoritativeFiles ?? [], errors)
|
|
672
|
+
checkFileRecords(root, 'generatedFiles', manifest.generatedFiles ?? [], errors, { sourcePathRequired: true })
|
|
673
|
+
for (const documentationPath of manifest.documentationFiles ?? []) {
|
|
674
|
+
if (!isRegularFile(path.join(root, documentationPath))) {
|
|
675
|
+
errors.push(`documentationFiles ${documentationPath} is not an exact regular file in the working tree`)
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (classification.changeClass === 'asset-sync' || manifest.changeClass === 'asset-sync') {
|
|
680
|
+
for (const record of classification.records) {
|
|
681
|
+
if (record.contract) errors.push(`asset-sync run touches ${record.path}, which carries the ${record.contract} contract`)
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (classification.changeClass === 'asset-sync') {
|
|
685
|
+
if (input.baseSha) {
|
|
686
|
+
for (const generated of manifest.generatedFiles ?? []) {
|
|
687
|
+
const baseHash = baseFileHash(root, input.baseSha, generated.sourcePath)
|
|
688
|
+
const headAbsolute = path.join(root, generated.sourcePath)
|
|
689
|
+
const headHash = isRegularFile(headAbsolute) ? hashFile(headAbsolute) : null
|
|
690
|
+
if (baseHash !== null && headHash !== null && baseHash !== headHash) {
|
|
691
|
+
errors.push(
|
|
692
|
+
`asset-sync run changed authoritative source ${generated.sourcePath}; ` +
|
|
693
|
+
'a semantic source change is a knowledge-contract change',
|
|
694
|
+
)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (classification.changeClass === 'knowledge-contract') {
|
|
701
|
+
const declaredTests = Array.isArray(manifest.focusedTestFiles) ? manifest.focusedTestFiles.map(normalizeRelativePath) : []
|
|
702
|
+
const changedTests = declaredTests.filter((testPath) => classification.focusedTestPaths.includes(testPath))
|
|
703
|
+
if (!declaredTests.length) {
|
|
704
|
+
errors.push('a knowledge-contract change requires at least one focused test that demonstrates the affected contract')
|
|
705
|
+
}
|
|
706
|
+
for (const testPath of declaredTests) {
|
|
707
|
+
if (!isFocusedTestPath(testPath)) errors.push(`focusedTestFiles ${testPath} is not a test file`)
|
|
708
|
+
if (!isRegularFile(path.join(root, testPath))) errors.push(`focusedTestFiles ${testPath} is not an exact regular file in the working tree`)
|
|
709
|
+
}
|
|
710
|
+
if (declaredTests.length && !changedTests.length) {
|
|
711
|
+
errors.push('no declared focused test appears in the diff; a knowledge-contract change must add or repair its own regression')
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const cases = input.cases ?? readJsonIfPresent(path.join(root, input.harnessRelativeDir ?? '', 'cases.json'))
|
|
715
|
+
if (!Array.isArray(cases)) {
|
|
716
|
+
errors.push('the case catalog could not be read, so affected cases and the catalog count cannot be verified')
|
|
717
|
+
} else {
|
|
718
|
+
const knownCaseIds = new Set(cases.map((entry) => entry?.id))
|
|
719
|
+
for (const caseId of manifest.affectedCaseIds ?? []) {
|
|
720
|
+
if (!knownCaseIds.has(caseId)) errors.push(`affectedCaseIds ${caseId} does not exist in the case catalog`)
|
|
721
|
+
}
|
|
722
|
+
if (manifest.expectedCatalogCount !== cases.length) {
|
|
723
|
+
errors.push(`expectedCatalogCount ${manifest.expectedCatalogCount} does not match the catalog's ${cases.length} cases`)
|
|
724
|
+
}
|
|
725
|
+
for (const rangeId of manifest.affectedRanges ?? []) {
|
|
726
|
+
const expanded = expandCaseRange(rangeId)
|
|
727
|
+
if (!expanded) { errors.push(`affectedRanges ${rangeId} is not an ascending OMH range`); continue }
|
|
728
|
+
const missing = expanded.filter((caseId) => !knownCaseIds.has(caseId))
|
|
729
|
+
if (missing.length) errors.push(`affectedRanges ${rangeId} covers cases absent from the catalog: ${missing.join(', ')}`)
|
|
730
|
+
}
|
|
731
|
+
const rangeMembers = new Set((manifest.affectedRanges ?? []).flatMap((rangeId) => expandCaseRange(rangeId) ?? []))
|
|
732
|
+
for (const caseId of manifest.affectedCaseIds ?? []) {
|
|
733
|
+
if (rangeMembers.size && !rangeMembers.has(caseId)) {
|
|
734
|
+
errors.push(`affectedCaseIds ${caseId} falls outside every declared affected range`)
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const releaseMatrix = input.releaseMatrix ?? readJsonIfPresent(path.join(root, input.harnessRelativeDir ?? '', 'release-matrix.json'))
|
|
740
|
+
if (!isPlainObject(releaseMatrix)) {
|
|
741
|
+
errors.push('the release matrix could not be read, so required lanes cannot be verified')
|
|
742
|
+
} else {
|
|
743
|
+
for (const lane of manifest.requiredReleaseLanes ?? []) {
|
|
744
|
+
if (!Object.hasOwn(releaseMatrix, lane)) errors.push(`requiredReleaseLanes ${lane} is not a lane in the release matrix`)
|
|
745
|
+
}
|
|
746
|
+
if (!(manifest.requiredReleaseLanes ?? []).length) {
|
|
747
|
+
errors.push('a knowledge-contract change must name at least one affected certified release lane')
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const executionErrors = assertFocusedExecutionEvidence(declaredTests, input.executionEvidence)
|
|
752
|
+
derived.focusedExecutions = input.executionEvidence?.executions ?? []
|
|
753
|
+
errors.push(...executionErrors)
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const linkedContracts = ['source-link', 'example-source', 'installed-source']
|
|
757
|
+
const needsSourceLinkInventory = classification.changedContracts.some((contract) => linkedContracts.includes(contract))
|
|
758
|
+
derived.sourceLinkInventoryRequired = needsSourceLinkInventory
|
|
759
|
+
if (needsSourceLinkInventory) {
|
|
760
|
+
const declaredInventory = manifest.sourceLinkInventory?.path
|
|
761
|
+
if (declaredInventory && declaredInventory !== SOURCE_LINK_INVENTORY_PATH) {
|
|
762
|
+
errors.push(`sourceLinkInventory.path must be ${SOURCE_LINK_INVENTORY_PATH}`)
|
|
763
|
+
}
|
|
764
|
+
const inventoryPath = declaredInventory ?? SOURCE_LINK_INVENTORY_PATH
|
|
765
|
+
if (!isRegularFile(path.join(root, inventoryPath))) {
|
|
766
|
+
derived.sourceLinkInventoryStatus = 'absent'
|
|
767
|
+
errors.push(CANON_C_REASON)
|
|
768
|
+
} else {
|
|
769
|
+
derived.sourceLinkInventoryStatus = 'present'
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
checkExampleSourceMirrors(root, classification.records, errors, derived)
|
|
774
|
+
|
|
775
|
+
return { ok: errors.length === 0, errors, derived }
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function writeJsonAtomic(absolutePath, value) {
|
|
779
|
+
const directory = path.dirname(absolutePath)
|
|
780
|
+
fs.mkdirSync(directory, { recursive: true })
|
|
781
|
+
const temporaryPath = path.join(directory, `.${path.basename(absolutePath)}.${process.pid}.tmp`)
|
|
782
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`)
|
|
783
|
+
fs.renameSync(temporaryPath, absolutePath)
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function usage() {
|
|
787
|
+
return [
|
|
788
|
+
'Usage: validate-knowledge-change --manifest <path> --base <ref> [--root <dir>] [--harness-dir <dir>]',
|
|
789
|
+
' [--out <path>] [--execution-timeout <ms>]',
|
|
790
|
+
'',
|
|
791
|
+
'Derives the knowledge-contract/asset-sync class from the diff between <ref> and the working tree,',
|
|
792
|
+
'validates the authored run manifest against knowledge-change.schema.json, and — for a knowledge-contract',
|
|
793
|
+
'change — runs each declared focused test once in an isolated base-plus-test-only worktree and once at head,',
|
|
794
|
+
'recording exit codes and output hashes as controller-owned evidence. The result is written atomically.',
|
|
795
|
+
].join('\n')
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function parseArgs(argv) {
|
|
799
|
+
const options = { root: process.cwd(), help: false }
|
|
800
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
801
|
+
const token = argv[index]
|
|
802
|
+
if (token === '--help' || token === '-h') { options.help = true; continue }
|
|
803
|
+
const value = argv[index + 1]
|
|
804
|
+
if (token === '--manifest') { options.manifest = value; index += 1; continue }
|
|
805
|
+
if (token === '--base') { options.base = value; index += 1; continue }
|
|
806
|
+
if (token === '--root') { options.root = value; index += 1; continue }
|
|
807
|
+
if (token === '--harness-dir') { options.harnessDir = value; index += 1; continue }
|
|
808
|
+
if (token === '--out') { options.out = value; index += 1; continue }
|
|
809
|
+
if (token === '--execution-timeout') {
|
|
810
|
+
const parsed = Number(value)
|
|
811
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error('--execution-timeout must be a positive integer of milliseconds')
|
|
812
|
+
options.executionTimeoutMs = parsed
|
|
813
|
+
index += 1
|
|
814
|
+
continue
|
|
815
|
+
}
|
|
816
|
+
throw new Error(`unknown argument: ${token}`)
|
|
817
|
+
}
|
|
818
|
+
return options
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function defaultHarnessRelativeDir(root) {
|
|
822
|
+
const candidates = ['packages/create-app/agentic/shared/ai/harness', 'agentic/shared/ai/harness', '.ai/harness']
|
|
823
|
+
return candidates.find((candidate) => isRegularFile(path.join(root, candidate, 'cases.json'))) ?? '.ai/harness'
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
export function main(argv = process.argv.slice(2)) {
|
|
827
|
+
let options
|
|
828
|
+
try { options = parseArgs(argv) } catch (error) { console.error(error.message); console.error(usage()); return EXIT_INVALID }
|
|
829
|
+
if (options.help) { console.log(usage()); return EXIT_PASS }
|
|
830
|
+
if (!options.manifest || !options.base) { console.error('--manifest and --base are required'); console.error(usage()); return EXIT_INVALID }
|
|
831
|
+
|
|
832
|
+
const root = path.resolve(options.root)
|
|
833
|
+
const harnessRelativeDir = options.harnessDir ?? defaultHarnessRelativeDir(root)
|
|
834
|
+
const schemaCandidates = [
|
|
835
|
+
path.join(root, harnessRelativeDir, SCHEMA_BASENAME),
|
|
836
|
+
fileURLToPath(new URL(`../ai/harness/${SCHEMA_BASENAME}`, import.meta.url)),
|
|
837
|
+
fileURLToPath(new URL(`../.ai/harness/${SCHEMA_BASENAME}`, import.meta.url)),
|
|
838
|
+
]
|
|
839
|
+
const schemaPath = schemaCandidates.find((candidate) => isRegularFile(candidate))
|
|
840
|
+
if (!schemaPath) { console.error(`knowledge-change schema not found: ${schemaCandidates.join(', ')}`); return EXIT_INVALID }
|
|
841
|
+
const schema = readJsonIfPresent(schemaPath)
|
|
842
|
+
if (!schema) { console.error(`knowledge-change schema is not valid JSON: ${schemaPath}`); return EXIT_INVALID }
|
|
843
|
+
|
|
844
|
+
const manifestPath = path.resolve(root, options.manifest)
|
|
845
|
+
const manifest = readJsonIfPresent(manifestPath)
|
|
846
|
+
if (manifest === null) { console.error(`run manifest not found or not valid JSON: ${manifestPath}`); return EXIT_INVALID }
|
|
847
|
+
|
|
848
|
+
let baseSha
|
|
849
|
+
let headSha
|
|
850
|
+
let changedPaths
|
|
851
|
+
try {
|
|
852
|
+
baseSha = resolveGitSha(root, options.base)
|
|
853
|
+
headSha = resolveGitSha(root, 'HEAD')
|
|
854
|
+
changedPaths = deriveChangedPaths(root, baseSha)
|
|
855
|
+
} catch (error) { console.error(error.message); return EXIT_INVALID }
|
|
856
|
+
|
|
857
|
+
let baseRefError = null
|
|
858
|
+
if (manifest.baseRef !== options.base && manifest.baseRef !== baseSha) {
|
|
859
|
+
let authoredSha = null
|
|
860
|
+
try { authoredSha = resolveGitSha(root, manifest.baseRef) } catch { authoredSha = null }
|
|
861
|
+
if (authoredSha !== baseSha) {
|
|
862
|
+
baseRefError = `authored baseRef "${manifest.baseRef}" does not resolve to the --base SHA ${baseSha}`
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const classification = deriveChangeClassification(changedPaths)
|
|
867
|
+
let executionEvidence = null
|
|
868
|
+
if (classification.changeClass === 'knowledge-contract') {
|
|
869
|
+
executionEvidence = baseRefError
|
|
870
|
+
? { executions: [], errors: ['focused base/head execution was not attempted because the authored baseRef does not resolve to --base'] }
|
|
871
|
+
: runFocusedExecutions({
|
|
872
|
+
root,
|
|
873
|
+
baseSha,
|
|
874
|
+
headSha,
|
|
875
|
+
changedPaths,
|
|
876
|
+
testFiles: Array.isArray(manifest.focusedTestFiles) ? manifest.focusedTestFiles : [],
|
|
877
|
+
timeoutMs: options.executionTimeoutMs,
|
|
878
|
+
})
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const result = validateKnowledgeChange({
|
|
882
|
+
root, manifest, schema, changedPaths, baseSha, harnessRelativeDir, executionEvidence,
|
|
883
|
+
})
|
|
884
|
+
if (baseRefError) { result.errors.unshift(baseRefError); result.ok = false }
|
|
885
|
+
|
|
886
|
+
const completed = {
|
|
887
|
+
...manifest,
|
|
888
|
+
resolvedBaseSha: baseSha,
|
|
889
|
+
headSha,
|
|
890
|
+
focusedExecutions: executionEvidence?.executions ?? [],
|
|
891
|
+
}
|
|
892
|
+
const completedErrors = validateJsonSchema(completed, schema.$defs.completedManifest, '$', schema)
|
|
893
|
+
if (completedErrors.length) {
|
|
894
|
+
result.errors.push(...completedErrors.map((error) => `completed manifest schema: ${error}`))
|
|
895
|
+
result.ok = false
|
|
896
|
+
}
|
|
897
|
+
const outputPath = path.resolve(root, options.out ?? `${options.manifest}.result.json`)
|
|
898
|
+
writeJsonAtomic(outputPath, {
|
|
899
|
+
status: result.ok ? 'pass' : 'fail',
|
|
900
|
+
manifest: completed,
|
|
901
|
+
derived: result.derived,
|
|
902
|
+
changedPaths,
|
|
903
|
+
errors: result.errors,
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
for (const error of result.errors) console.error(`knowledge-change: ${error}`)
|
|
907
|
+
console.log(`${result.ok ? 'PASS' : 'FAIL'} knowledge-change ${result.derived.changeClass} — ${outputPath}`)
|
|
908
|
+
return result.ok ? EXIT_PASS : EXIT_FAILURE
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) process.exitCode = main()
|