@warpgogol/forge 4.2.4 → 5.1.1
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/AGENTS.md +14 -6
- package/CHANGELOG.md +72 -0
- package/README.md +8 -8
- package/README.uk.md +5 -5
- package/dist/os/adr/handlers/list-create.d.ts.map +1 -1
- package/dist/os/adr/handlers/list-create.js +4 -1
- package/dist/os/adr/handlers/list-create.js.map +1 -1
- package/dist/os/adr/handlers/validate.d.ts.map +1 -1
- package/dist/os/adr/handlers/validate.js +6 -1
- package/dist/os/adr/handlers/validate.js.map +1 -1
- package/dist/os/adr/types.d.ts.map +1 -1
- package/dist/os/adr/types.js +7 -3
- package/dist/os/adr/types.js.map +1 -1
- package/dist/os/compass/compass.module.d.ts.map +1 -1
- package/dist/os/compass/compass.module.js +76 -10
- package/dist/os/compass/compass.module.js.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.js +7 -3
- package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
- package/dist/os/compass/handlers/compass-change-summary-handler.d.ts +1 -9
- package/dist/os/compass/handlers/compass-change-summary-handler.d.ts.map +1 -1
- package/dist/os/compass/handlers/compass-change-summary-handler.js +44 -186
- package/dist/os/compass/handlers/compass-change-summary-handler.js.map +1 -1
- package/dist/os/compass/handlers/compass-inventory-handler.d.ts +2 -0
- package/dist/os/compass/handlers/compass-inventory-handler.d.ts.map +1 -1
- package/dist/os/compass/handlers/compass-inventory-handler.js +24 -12
- package/dist/os/compass/handlers/compass-inventory-handler.js.map +1 -1
- package/dist/os/compass/handlers/compass-inventory.d.ts +7 -5
- package/dist/os/compass/handlers/compass-inventory.d.ts.map +1 -1
- package/dist/os/compass/handlers/compass-inventory.js +84 -241
- package/dist/os/compass/handlers/compass-inventory.js.map +1 -1
- package/dist/os/compass/handlers/compass-migrate-handler.d.ts +4 -0
- package/dist/os/compass/handlers/compass-migrate-handler.d.ts.map +1 -0
- package/dist/os/compass/handlers/compass-migrate-handler.js +100 -0
- package/dist/os/compass/handlers/compass-migrate-handler.js.map +1 -0
- package/dist/os/compass/handlers/compass-migrate.d.ts +34 -0
- package/dist/os/compass/handlers/compass-migrate.d.ts.map +1 -0
- package/dist/os/compass/handlers/compass-migrate.js +273 -0
- package/dist/os/compass/handlers/compass-migrate.js.map +1 -0
- package/dist/os/compass/handlers/resolve-scan-root.d.ts.map +1 -1
- package/dist/os/compass/handlers/resolve-scan-root.js +15 -8
- package/dist/os/compass/handlers/resolve-scan-root.js.map +1 -1
- package/dist/os/compass/handlers/summary-record.d.ts +46 -0
- package/dist/os/compass/handlers/summary-record.d.ts.map +1 -0
- package/dist/os/compass/handlers/summary-record.js +233 -0
- package/dist/os/compass/handlers/summary-record.js.map +1 -0
- package/dist/os/compass/index.d.ts +4 -1
- package/dist/os/compass/index.d.ts.map +1 -1
- package/dist/os/compass/index.js +9 -1
- package/dist/os/compass/index.js.map +1 -1
- package/dist/os/compass/policy.d.ts +65 -0
- package/dist/os/compass/policy.d.ts.map +1 -0
- package/dist/os/compass/policy.js +241 -0
- package/dist/os/compass/policy.js.map +1 -0
- package/dist/os/core/core.module.d.ts.map +1 -1
- package/dist/os/core/core.module.js +11 -23
- package/dist/os/core/core.module.js.map +1 -1
- package/dist/os/core/handlers/forge-autonomy-validate.d.ts.map +1 -1
- package/dist/os/core/handlers/forge-autonomy-validate.js +7 -1
- package/dist/os/core/handlers/forge-autonomy-validate.js.map +1 -1
- package/dist/os/core/handlers/package-health.d.ts.map +1 -1
- package/dist/os/core/handlers/package-health.js +6 -1
- package/dist/os/core/handlers/package-health.js.map +1 -1
- package/dist/os/core/handlers/pinned-check.d.ts.map +1 -1
- package/dist/os/core/handlers/pinned-check.js +6 -1
- package/dist/os/core/handlers/pinned-check.js.map +1 -1
- package/dist/os/core/handlers/pinned-validate.d.ts.map +1 -1
- package/dist/os/core/handlers/pinned-validate.js +6 -1
- package/dist/os/core/handlers/pinned-validate.js.map +1 -1
- package/dist/os/core/handlers/validate.d.ts.map +1 -1
- package/dist/os/core/handlers/validate.js +6 -1
- package/dist/os/core/handlers/validate.js.map +1 -1
- package/dist/os/exploration/exploration.module.d.ts.map +1 -1
- package/dist/os/exploration/exploration.module.js +6 -4
- package/dist/os/exploration/exploration.module.js.map +1 -1
- package/dist/os/exploration/handlers/list.d.ts.map +1 -1
- package/dist/os/exploration/handlers/list.js +4 -1
- package/dist/os/exploration/handlers/list.js.map +1 -1
- package/dist/os/mission/handlers/archive.d.ts.map +1 -1
- package/dist/os/mission/handlers/archive.js +7 -3
- package/dist/os/mission/handlers/archive.js.map +1 -1
- package/dist/os/mission/index.d.ts.map +1 -1
- package/dist/os/mission/index.js +13 -0
- package/dist/os/mission/index.js.map +1 -1
- package/dist/os/notes/index.d.ts.map +1 -1
- package/dist/os/notes/index.js +7 -1
- package/dist/os/notes/index.js.map +1 -1
- package/dist/os/rfc/frontmatter-io.d.ts.map +1 -1
- package/dist/os/rfc/frontmatter-io.js +6 -1
- package/dist/os/rfc/frontmatter-io.js.map +1 -1
- package/dist/os/rfc/handlers/archive.d.ts.map +1 -1
- package/dist/os/rfc/handlers/archive.js +6 -1
- package/dist/os/rfc/handlers/archive.js.map +1 -1
- package/dist/os/rfc/handlers/implement-stamp.d.ts.map +1 -1
- package/dist/os/rfc/handlers/implement-stamp.js +7 -3
- package/dist/os/rfc/handlers/implement-stamp.js.map +1 -1
- package/dist/os/rfc/handlers/index-graph.d.ts.map +1 -1
- package/dist/os/rfc/handlers/index-graph.js +6 -1
- package/dist/os/rfc/handlers/index-graph.js.map +1 -1
- package/dist/os/rfc/handlers/lifecycle.d.ts.map +1 -1
- package/dist/os/rfc/handlers/lifecycle.js +6 -1
- package/dist/os/rfc/handlers/lifecycle.js.map +1 -1
- package/dist/os/rfc/handlers/list-create.d.ts.map +1 -1
- package/dist/os/rfc/handlers/list-create.js +6 -1
- package/dist/os/rfc/handlers/list-create.js.map +1 -1
- package/dist/os/rfc/handlers/pipeline-status.d.ts.map +1 -1
- package/dist/os/rfc/handlers/pipeline-status.js +6 -1
- package/dist/os/rfc/handlers/pipeline-status.js.map +1 -1
- package/dist/os/rfc/handlers/supersede-propose.d.ts.map +1 -1
- package/dist/os/rfc/handlers/supersede-propose.js +6 -1
- package/dist/os/rfc/handlers/supersede-propose.js.map +1 -1
- package/dist/os/rfc/handlers/validate-rules.d.ts.map +1 -1
- package/dist/os/rfc/handlers/validate-rules.js +7 -3
- package/dist/os/rfc/handlers/validate-rules.js.map +1 -1
- package/dist/os/rfc/handlers/validate.d.ts.map +1 -1
- package/dist/os/rfc/handlers/validate.js +6 -1
- package/dist/os/rfc/handlers/validate.js.map +1 -1
- package/dist/os/rfc/index.d.ts.map +1 -1
- package/dist/os/rfc/index.js +6 -1
- package/dist/os/rfc/index.js.map +1 -1
- package/dist/os/rfc/rfc.module.d.ts.map +1 -1
- package/dist/os/rfc/rfc.module.js +6 -1
- package/dist/os/rfc/rfc.module.js.map +1 -1
- package/dist/os/rfc/types.d.ts +7 -4
- package/dist/os/rfc/types.d.ts.map +1 -1
- package/dist/os/rfc/types.js +7 -4
- package/dist/os/rfc/types.js.map +1 -1
- package/dist/os/session/handlers/save.d.ts.map +1 -1
- package/dist/os/session/handlers/save.js +6 -1
- package/dist/os/session/handlers/save.js.map +1 -1
- package/dist/os/werkstatt/handlers/werkstatt-lock-recover.d.ts.map +1 -1
- package/dist/os/werkstatt/handlers/werkstatt-lock-recover.js +6 -1
- package/dist/os/werkstatt/handlers/werkstatt-lock-recover.js.map +1 -1
- package/dist/os/werkstatt/handlers/workspace-deps.js +3 -3
- package/dist/os/werkstatt/handlers/workspace-deps.js.map +1 -1
- package/dist/os/workflow/index.d.ts.map +1 -1
- package/dist/os/workflow/index.js +6 -1
- package/dist/os/workflow/index.js.map +1 -1
- package/dist/src/config/forge-config.d.ts +62 -4
- package/dist/src/config/forge-config.d.ts.map +1 -1
- package/dist/src/config/forge-config.js +35 -19
- package/dist/src/config/forge-config.js.map +1 -1
- package/dist/src/forge-module.d.ts.map +1 -1
- package/dist/src/forge-module.js +6 -1
- package/dist/src/forge-module.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +10 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/migration-adapters/git-utils.d.ts.map +1 -1
- package/dist/src/migration-adapters/git-utils.js +6 -2
- package/dist/src/migration-adapters/git-utils.js.map +1 -1
- package/dist/src/migration-adapters/ignored-files.d.ts.map +1 -1
- package/dist/src/migration-adapters/ignored-files.js +6 -1
- package/dist/src/migration-adapters/ignored-files.js.map +1 -1
- package/dist/src/migration-adapters/index.d.ts.map +1 -1
- package/dist/src/migration-adapters/index.js +4 -1
- package/dist/src/migration-adapters/index.js.map +1 -1
- package/dist/src/migration-adapters/types.d.ts.map +1 -1
- package/dist/src/migration-adapters/types.js +6 -2
- package/dist/src/migration-adapters/types.js.map +1 -1
- package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
- package/dist/src/onboarding/agents-generate.js +18 -14
- package/dist/src/onboarding/agents-generate.js.map +1 -1
- package/dist/src/onboarding/create.d.ts.map +1 -1
- package/dist/src/onboarding/create.js +7 -6
- package/dist/src/onboarding/create.js.map +1 -1
- package/dist/src/onboarding/doctor.d.ts.map +1 -1
- package/dist/src/onboarding/doctor.js +7 -13
- package/dist/src/onboarding/doctor.js.map +1 -1
- package/dist/src/onboarding/init.d.ts.map +1 -1
- package/dist/src/onboarding/init.js +7 -12
- package/dist/src/onboarding/init.js.map +1 -1
- package/dist/src/onboarding/invariant-engine.d.ts.map +1 -1
- package/dist/src/onboarding/invariant-engine.js +7 -2
- package/dist/src/onboarding/invariant-engine.js.map +1 -1
- package/dist/src/onboarding/nested-agents-generate.d.ts.map +1 -1
- package/dist/src/onboarding/nested-agents-generate.js +7 -2
- package/dist/src/onboarding/nested-agents-generate.js.map +1 -1
- package/dist/src/onboarding/nested-agents-templates.d.ts.map +1 -1
- package/dist/src/onboarding/nested-agents-templates.js +11 -3
- package/dist/src/onboarding/nested-agents-templates.js.map +1 -1
- package/dist/src/onboarding/scaffold-project.d.ts.map +1 -1
- package/dist/src/onboarding/scaffold-project.js +6 -1
- package/dist/src/onboarding/scaffold-project.js.map +1 -1
- package/dist/src/onboarding/upgrade.d.ts.map +1 -1
- package/dist/src/onboarding/upgrade.js +7 -3
- package/dist/src/onboarding/upgrade.js.map +1 -1
- package/dist/src/profiles/profile-schema.d.ts +54 -4
- package/dist/src/profiles/profile-schema.d.ts.map +1 -1
- package/dist/src/profiles/profile-schema.js +34 -14
- package/dist/src/profiles/profile-schema.js.map +1 -1
- package/dist/src/profiles/stack-profile.d.ts +24 -0
- package/dist/src/profiles/stack-profile.d.ts.map +1 -1
- package/dist/src/profiles/stack-profile.js +7 -3
- package/dist/src/profiles/stack-profile.js.map +1 -1
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +7 -9
- package/dist/src/registry.js.map +1 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +6 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/fs-atomic.d.ts.map +1 -1
- package/dist/src/utils/fs-atomic.js +13 -1
- package/dist/src/utils/fs-atomic.js.map +1 -1
- package/dist/src/utils/fs-idempotent.d.ts.map +1 -1
- package/dist/src/utils/fs-idempotent.js +13 -0
- package/dist/src/utils/fs-idempotent.js.map +1 -1
- package/dist/src/utils/fs-trash-sync.d.ts.map +1 -1
- package/dist/src/utils/fs-trash-sync.js +13 -1
- package/dist/src/utils/fs-trash-sync.js.map +1 -1
- package/dist/src/utils/fs-trash.d.ts.map +1 -1
- package/dist/src/utils/fs-trash.js +13 -1
- package/dist/src/utils/fs-trash.js.map +1 -1
- package/dist/src/utils/fs.d.ts.map +1 -1
- package/dist/src/utils/fs.js +14 -3
- package/dist/src/utils/fs.js.map +1 -1
- package/dist/src/utils/hash.d.ts.map +1 -1
- package/dist/src/utils/hash.js +12 -1
- package/dist/src/utils/hash.js.map +1 -1
- package/dist/src/utils/index.d.ts.map +1 -1
- package/dist/src/utils/index.js +13 -2
- package/dist/src/utils/index.js.map +1 -1
- package/dist/src/utils/string-utils.d.ts.map +1 -1
- package/dist/src/utils/string-utils.js +12 -1
- package/dist/src/utils/string-utils.js.map +1 -1
- package/dist/src/validators/port-validate.js +1 -1
- package/dist/src/validators/port-validate.js.map +1 -1
- package/dist/src/validators/skill-validate.d.ts.map +1 -1
- package/dist/src/validators/skill-validate.js +7 -8
- package/dist/src/validators/skill-validate.js.map +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/reference/forge-yaml.md +38 -9
- package/docs/reference/profiles.md +8 -8
- package/os/adr/handlers/list-create.ts +4 -1
- package/os/adr/handlers/validate.ts +6 -1
- package/os/adr/types.ts +7 -3
- package/os/compass/compass.module.ts +79 -13
- package/os/compass/handlers/compass-audit-handler.ts +22 -3
- package/os/compass/handlers/compass-change-summary-handler.ts +55 -227
- package/os/compass/handlers/compass-inventory-handler.ts +30 -12
- package/os/compass/handlers/compass-inventory.ts +103 -240
- package/os/compass/handlers/compass-migrate-handler.ts +122 -0
- package/os/compass/handlers/compass-migrate.ts +361 -0
- package/os/compass/handlers/resolve-scan-root-package.test.ts +70 -0
- package/os/compass/handlers/resolve-scan-root.ts +15 -8
- package/os/compass/handlers/summary-record.ts +313 -0
- package/os/compass/handlers/tests/compass-migrate.test.ts +291 -0
- package/os/compass/handlers/tests/compass-policy.test.ts +249 -0
- package/os/compass/handlers/tests/compass-v2-contract.test.ts +9 -9
- package/os/compass/handlers/tests/summary-record.test.ts +224 -0
- package/os/compass/index.ts +23 -5
- package/os/compass/policy.ts +350 -0
- package/os/core/core.module.ts +11 -23
- package/os/core/handlers/forge-autonomy-validate.ts +7 -1
- package/os/core/handlers/package-health.ts +6 -1
- package/os/core/handlers/pinned-check.ts +6 -1
- package/os/core/handlers/pinned-validate.ts +6 -1
- package/os/core/handlers/validate.ts +6 -1
- package/os/exploration/exploration.module.ts +72 -71
- package/os/exploration/handlers/list.ts +4 -1
- package/os/mission/handlers/archive.ts +7 -3
- package/os/mission/index.ts +14 -0
- package/os/notes/index.ts +7 -1
- package/os/rfc/frontmatter-io.ts +6 -1
- package/os/rfc/handlers/archive.ts +6 -1
- package/os/rfc/handlers/implement-stamp.ts +7 -3
- package/os/rfc/handlers/index-graph.ts +6 -1
- package/os/rfc/handlers/lifecycle.ts +6 -1
- package/os/rfc/handlers/list-create.ts +6 -1
- package/os/rfc/handlers/pipeline-status.ts +6 -1
- package/os/rfc/handlers/supersede-propose.ts +6 -1
- package/os/rfc/handlers/validate-rules.ts +7 -3
- package/os/rfc/handlers/validate.ts +6 -1
- package/os/rfc/index.ts +6 -1
- package/os/rfc/rfc.module.ts +6 -1
- package/os/rfc/types.ts +7 -4
- package/os/session/handlers/save.ts +6 -1
- package/os/werkstatt/handlers/werkstatt-lock-recover.ts +6 -1
- package/os/werkstatt/handlers/workspace-deps.ts +3 -3
- package/os/workflow/index.ts +6 -1
- package/package.json +1 -1
- package/profiles/{godot-csharp.yaml → godot-game.yaml} +2 -2
- package/profiles/{knowledge-typescript-turborepo.yaml → knowledge.yaml} +1 -1
- package/profiles/{phaser-turborepo.yaml → phaser-game.yaml} +2 -2
- package/profiles/{typescript-turborepo.yaml → typescript.yaml} +2 -2
- package/skills/fo/fo-add-tests/SKILL.md +14 -0
- package/skills/fo/fo-architecture/SKILL.md +14 -0
- package/skills/fo/fo-compass-annotate/SKILL.md +14 -0
- package/skills/fo/fo-design-summit/SKILL.md +14 -0
- package/skills/fo/fo-doc-audit/SKILL.md +14 -0
- package/skills/fo/fo-explore/SKILL.md +14 -0
- package/skills/fo/fo-extract-dna/SKILL.md +14 -0
- package/skills/fo/fo-fix/SKILL.md +14 -0
- package/skills/fo/fo-handoff/SKILL.md +14 -0
- package/skills/fo/fo-harvest/SKILL.md +14 -0
- package/skills/fo/fo-idea/SKILL.md +14 -0
- package/skills/fo/fo-idea-audit/SKILL.md +14 -0
- package/skills/fo/fo-idea-create-adr/SKILL.md +14 -0
- package/skills/fo/fo-idea-create-rfc/SKILL.md +14 -0
- package/skills/fo/fo-idea-enhance/SKILL.md +14 -0
- package/skills/fo/fo-idea-i-just-want-to-see-the-plan/SKILL.md +14 -0
- package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +14 -0
- package/skills/fo/fo-idea-implement/SKILL.md +14 -0
- package/skills/fo/fo-idea-plan/SKILL.md +14 -0
- package/skills/fo/fo-idea-status/SKILL.md +14 -0
- package/skills/fo/fo-knowledge-distill/SKILL.md +14 -0
- package/skills/fo/fo-memory-sync/SKILL.md +14 -0
- package/skills/fo/fo-qa/SKILL.md +14 -0
- package/skills/fo/fo-review/SKILL.md +14 -0
- package/skills/fo/fo-session-retro/SKILL.md +14 -0
- package/skills/fo/fo-session-save/SKILL.md +14 -0
- package/skills/fo/fo-spec-ingest/SKILL.md +14 -0
- package/skills/fo/fo-step-commit/SKILL.md +14 -0
- package/skills/fo/fo-triage/SKILL.md +14 -0
- package/skills/meta/forge-bootstrap/SKILL.md +14 -0
- package/skills/meta/port-to-forge/SKILL.md +14 -0
- package/skills/meta/skill-create/SKILL.md +14 -0
- package/skills/shared/grilling/SKILL.md +14 -0
- package/skills/shared/my-preferences/SKILL.md +14 -0
- package/skills/shared/windows-ai-tooling/SKILL.md +14 -0
- package/skills/shared/writing-great-skills/SKILL.md +14 -0
- package/src/config/forge-config.ts +40 -23
- package/src/forge-module.ts +6 -1
- package/src/index.ts +10 -2
- package/src/migration-adapters/git-utils.ts +6 -2
- package/src/migration-adapters/ignored-files.ts +6 -1
- package/src/migration-adapters/index.ts +4 -1
- package/src/migration-adapters/types.ts +6 -2
- package/src/onboarding/agents-generate.ts +23 -14
- package/src/onboarding/create.ts +7 -6
- package/src/onboarding/doctor.ts +7 -13
- package/src/onboarding/init.ts +7 -12
- package/src/onboarding/invariant-engine.ts +7 -2
- package/src/onboarding/nested-agents-generate.ts +7 -2
- package/src/onboarding/nested-agents-templates.ts +11 -3
- package/src/onboarding/scaffold-project.ts +6 -1
- package/src/onboarding/upgrade.ts +7 -3
- package/src/profiles/profile-schema.ts +42 -18
- package/src/profiles/stack-profile.ts +7 -3
- package/src/registry.ts +7 -9
- package/src/tests/agents-generate-domain.test.ts +3 -3
- package/src/tests/agents-generate.test.ts +2 -2
- package/src/tests/create.test.ts +4 -4
- package/src/tests/forge-autonomy-validate.test.ts +1 -1
- package/src/tests/package-files.test.ts +53 -1
- package/src/tests/profile-schema.test.ts +4 -4
- package/src/tests/scaffold-project.test.ts +3 -3
- package/src/tests/stack-profile.test.ts +12 -12
- package/src/types.ts +6 -1
- package/src/utils/fs-atomic.ts +13 -1
- package/src/utils/fs-idempotent.ts +13 -0
- package/src/utils/fs-trash-sync.ts +13 -1
- package/src/utils/fs-trash.ts +13 -1
- package/src/utils/fs.ts +14 -3
- package/src/utils/hash.ts +12 -1
- package/src/utils/index.ts +13 -2
- package/src/utils/string-utils.ts +12 -1
- package/src/validators/port-validate.ts +1 -1
- package/src/validators/skill-validate.ts +7 -8
- package/skills/shared/extension-e2e-mitm/SKILL.md +0 -53
- package/skills/shared/extension-e2e-mitm/fixture-proxy.ts +0 -113
- package/src/types/werkstatt-engine-shims.d.ts +0 -1416
- package/src/types/werkstatt-shared-shims.d.ts +0 -5686
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>compass.summary.record — commit-time append of governance-referencing
|
|
4
|
+
items to CHANGE_SUMMARY blocks per RFC-1095. Collapses the 5-item window into
|
|
5
|
+
<history>, dedupes by ID+text, and reminds about KEY_DECISIONS on risk files.</purpose>
|
|
6
|
+
<non-goals>
|
|
7
|
+
<item>Do not create CHANGE_SUMMARY blocks — headers are authored by fo-compass-annotate.</item>
|
|
8
|
+
<item>Do not validate semantic quality of item text — record writes what the commit declares.</item>
|
|
9
|
+
<item>Do not import @warpgogol/* packages — this handler is autonomous (FORGE-AUTONOMY-01).</item>
|
|
10
|
+
</non-goals>
|
|
11
|
+
</MODULE_CONTRACT>
|
|
12
|
+
<KEY_DECISIONS>
|
|
13
|
+
<item>The 5-item window collapses oldest-first into history — history holds IDs only, never prose.</item>
|
|
14
|
+
<item>Recording is header-region guarded — a CHANGE_SUMMARY deeper than 120 lines is a doc example, not a header.</item>
|
|
15
|
+
<item>Item text is sanitized on record — literal Compass tags would corrupt history parsing.</item>
|
|
16
|
+
</KEY_DECISIONS>
|
|
17
|
+
<CHANGE_SUMMARY>
|
|
18
|
+
<item>RFC-1095: header-region guard in summary.record, restore test fixture with dynamic tags</item>
|
|
19
|
+
<item>RFC-1097: steps 1-4 — compass.migrate codemod
|
|
20
|
+
|
|
21
|
+
Add the v1 to v2 Compass header codemod: migrateFile pure transform (collapse, strip, seed, reorder, purpose-flag actions), migrateWorkspace walker, runCompassMigrate handler with dirty-tree refusal and --force/--files/--dry-run flags, module registration, and 15 unit tests.</item>
|
|
22
|
+
<item>RFC-1097: sweep — packages/forge + services clean
|
|
23
|
+
|
|
24
|
+
Sweep batch 2: real KEY_DECISIONS on 10 files, expanded purposes (CONTRACT-02/PURPOSE-02), headers on mission/index + gen-upstreams, sanitizeItemText in summary.record (literal Compass tags corrupted history), excludedPaths for wrangler types, test-fixtures testPattern. forge+services now 0 diagnostics under --mode error.</item>
|
|
25
|
+
<item>RFC-1097: sweep — engine package clean
|
|
26
|
+
|
|
27
|
+
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. engine package now 0 diagnostics.</item>
|
|
28
|
+
<item>RFC-1097: AC-4 banned literal in os/compass handlers
|
|
29
|
+
|
|
30
|
+
compass-migrate-handler hint used a consumer-specific run command — switched to generic 'pnpm exec forge run' convention. Reworded recorded CHANGE_SUMMARY items in 3 handlers to drop the consumer-specific literal. compass-policy AC-4 test green (65/65).</item>
|
|
31
|
+
<history>RFC-1095</history>
|
|
32
|
+
</CHANGE_SUMMARY>
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { existsSync } from "node:fs";
|
|
36
|
+
import { readFile } from "node:fs/promises";
|
|
37
|
+
import { resolve, relative } from "node:path";
|
|
38
|
+
import { detectRiskClass, getWorkspaceRelativeSegments } from "./compass-inventory.ts";
|
|
39
|
+
import { parseGovernanceIdParts, resolveCompassPolicy, type CompassPolicy } from "../policy.ts";
|
|
40
|
+
import { resolveCompassScanRoot } from "./resolve-scan-root.ts";
|
|
41
|
+
import { writeFileIfChanged } from "../../../src/utils/fs-idempotent.ts";
|
|
42
|
+
import type {
|
|
43
|
+
ForgeCommandInput,
|
|
44
|
+
ForgeCommandResult,
|
|
45
|
+
ForgeRuntimeContext,
|
|
46
|
+
} from "../../../src/types.ts";
|
|
47
|
+
|
|
48
|
+
const CHANGE_SUMMARY_BLOCK_RE = /<CHANGE_SUMMARY>[\s\S]*?<\/CHANGE_SUMMARY>/;
|
|
49
|
+
const ITEM_RE = /<item>([\s\S]*?)<\/item>/g;
|
|
50
|
+
const HISTORY_RE = /<history>([\s\S]*?)<\/history>/;
|
|
51
|
+
const CONVENTIONAL_PREFIX_RE = /^[a-z]+(\([^)]*\))?!?:\s*/i;
|
|
52
|
+
|
|
53
|
+
export const CHANGE_SUMMARY_WINDOW = 5;
|
|
54
|
+
|
|
55
|
+
// A CHANGE_SUMMARY block is a file header only when it starts within this many
|
|
56
|
+
// lines from the top. Deeper matches are examples inside template literals or
|
|
57
|
+
// fenced doc sections, not headers.
|
|
58
|
+
const HEADER_SCAN_LINES = 120;
|
|
59
|
+
|
|
60
|
+
export interface SummaryRecordInput {
|
|
61
|
+
id: string;
|
|
62
|
+
files: string[];
|
|
63
|
+
text?: string;
|
|
64
|
+
workpiece?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type SummaryRecordSkipReason = "no-block" | "duplicate" | "unparseable";
|
|
68
|
+
|
|
69
|
+
export interface SummaryRecordResult {
|
|
70
|
+
command: "compass.summary.record";
|
|
71
|
+
status: "pass";
|
|
72
|
+
recorded: string[];
|
|
73
|
+
collapsed: string[];
|
|
74
|
+
skipped: Array<{ file: string; reason: SummaryRecordSkipReason }>;
|
|
75
|
+
keyDecisionsReminders: string[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface ParsedChangeSummary {
|
|
79
|
+
items: string[];
|
|
80
|
+
historyIds: string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function parseChangeSummary(block: string): ParsedChangeSummary {
|
|
84
|
+
const items: string[] = [];
|
|
85
|
+
let m: RegExpExecArray | null;
|
|
86
|
+
const re = new RegExp(ITEM_RE.source, "g");
|
|
87
|
+
while ((m = re.exec(block)) !== null) {
|
|
88
|
+
items.push(m[1]!.trim());
|
|
89
|
+
}
|
|
90
|
+
const historyMatch = block.match(HISTORY_RE);
|
|
91
|
+
const historyIds = historyMatch
|
|
92
|
+
? historyMatch[1]!
|
|
93
|
+
.split(",")
|
|
94
|
+
.map((t) => t.trim())
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
: [];
|
|
97
|
+
return { items, historyIds };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeItemText(text: string): string {
|
|
101
|
+
return text.trim().replace(/\s+/g, " ");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function leadingGovernanceId(item: string, policy: CompassPolicy): string | null {
|
|
105
|
+
const match = item.match(policy.idPattern);
|
|
106
|
+
return match && match.index === 0 ? match[0] : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Merge existing + new history IDs: dedupe, per-namespace ascending numeric order. */
|
|
110
|
+
export function mergeHistoryIds(
|
|
111
|
+
existing: string[],
|
|
112
|
+
incoming: string[],
|
|
113
|
+
policy: CompassPolicy,
|
|
114
|
+
): string[] {
|
|
115
|
+
const all = [...new Set([...existing, ...incoming])].filter((id) =>
|
|
116
|
+
policy.idPatternFull.test(id),
|
|
117
|
+
);
|
|
118
|
+
return all.sort((a, b) => {
|
|
119
|
+
const pa = parseGovernanceIdParts(a)!;
|
|
120
|
+
const pb = parseGovernanceIdParts(b)!;
|
|
121
|
+
const ns = pa.namespace.localeCompare(pb.namespace);
|
|
122
|
+
return ns !== 0 ? ns : pa.numeric - pb.numeric;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getLineCommentPrefix(filePath: string): string | null {
|
|
127
|
+
if (filePath.endsWith(".gd")) return "# ";
|
|
128
|
+
if (filePath.endsWith(".tscn") || filePath.endsWith(".tres")) return "; ";
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function buildChangeSummaryBlock(
|
|
133
|
+
items: string[],
|
|
134
|
+
historyIds: string[],
|
|
135
|
+
filePath: string,
|
|
136
|
+
): string {
|
|
137
|
+
const prefix = getLineCommentPrefix(filePath);
|
|
138
|
+
const lines = ["<CHANGE_SUMMARY>"];
|
|
139
|
+
for (const item of items) {
|
|
140
|
+
lines.push(` <item>${item}</item>`);
|
|
141
|
+
}
|
|
142
|
+
if (historyIds.length > 0) {
|
|
143
|
+
lines.push(` <history>${historyIds.join(", ")}</history>`);
|
|
144
|
+
}
|
|
145
|
+
lines.push("</CHANGE_SUMMARY>");
|
|
146
|
+
if (prefix) {
|
|
147
|
+
return lines.map((line, i) => (i === 0 ? line : prefix + line)).join("\n");
|
|
148
|
+
}
|
|
149
|
+
return lines.join("\n");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function stripConventionalPrefix(subject: string): string {
|
|
153
|
+
return subject.replace(CONVENTIONAL_PREFIX_RE, "").trim();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Literal Compass block tags inside an item corrupt block parsing — a commit
|
|
157
|
+
// message saying "collapsed into history" would be matched by HISTORY_RE and
|
|
158
|
+
// produce phantom non-ID tokens (CS-07). Strip the angle brackets on record.
|
|
159
|
+
const COMPASS_TAG_LITERAL_RE =
|
|
160
|
+
/<\/?(?:CHANGE_SUMMARY|MODULE_CONTRACT|KEY_DECISIONS|history|item|purpose|non-goals)>/g;
|
|
161
|
+
|
|
162
|
+
export function sanitizeItemText(text: string): string {
|
|
163
|
+
return text.replace(COMPASS_TAG_LITERAL_RE, (tag) => tag.replace(/[<>]/g, ""));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function isValidGovernanceId(id: string, policy: CompassPolicy): boolean {
|
|
167
|
+
return policy.idPatternFull.test(id);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface RecordOutcome {
|
|
171
|
+
recorded: boolean;
|
|
172
|
+
collapsed: boolean;
|
|
173
|
+
skipReason?: SummaryRecordSkipReason;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Append `ID: text` to one file's CHANGE_SUMMARY and collapse the window.
|
|
178
|
+
* Returns the outcome; writes the file only when mutated and not dry-run.
|
|
179
|
+
*/
|
|
180
|
+
export async function recordSummaryItem(
|
|
181
|
+
absPath: string,
|
|
182
|
+
relPath: string,
|
|
183
|
+
id: string,
|
|
184
|
+
text: string,
|
|
185
|
+
dryRun: boolean,
|
|
186
|
+
policy: CompassPolicy,
|
|
187
|
+
): Promise<RecordOutcome> {
|
|
188
|
+
const source = await readFile(absPath, "utf8");
|
|
189
|
+
const blockMatch = source.match(CHANGE_SUMMARY_BLOCK_RE);
|
|
190
|
+
// Header-region guard: a CHANGE_SUMMARY is a header only when it sits at the
|
|
191
|
+
// top of the file. Blocks deeper in the source are examples inside template
|
|
192
|
+
// literals or fenced doc sections — recording into them corrupts the file.
|
|
193
|
+
const inHeaderRegion =
|
|
194
|
+
blockMatch !== null &&
|
|
195
|
+
blockMatch.index !== undefined &&
|
|
196
|
+
source.slice(0, blockMatch.index).split("\n").length <= HEADER_SCAN_LINES;
|
|
197
|
+
if (!blockMatch || !inHeaderRegion) {
|
|
198
|
+
return {
|
|
199
|
+
recorded: false,
|
|
200
|
+
collapsed: false,
|
|
201
|
+
skipReason: source.includes("<CHANGE_SUMMARY>") ? "unparseable" : "no-block",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const { items, historyIds } = parseChangeSummary(blockMatch[0]);
|
|
206
|
+
// Avoid "RFC-1095: RFC-1095 ..." when the text already leads with the same ID;
|
|
207
|
+
// a text equal to the bare ID collapses to the bare-ID item form.
|
|
208
|
+
const strippedText = text.startsWith(id)
|
|
209
|
+
? text.slice(id.length).replace(/^\s*[:—-]?\s*/, "")
|
|
210
|
+
: text;
|
|
211
|
+
const newItem = strippedText.length > 0 ? `${id}: ${sanitizeItemText(strippedText)}` : id;
|
|
212
|
+
const normalizedNew = normalizeItemText(newItem);
|
|
213
|
+
if (items.some((item) => normalizeItemText(item) === normalizedNew)) {
|
|
214
|
+
return { recorded: false, collapsed: false, skipReason: "duplicate" };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const nextItems = [...items, newItem];
|
|
218
|
+
const collapsedIds: string[] = [];
|
|
219
|
+
while (nextItems.length > CHANGE_SUMMARY_WINDOW) {
|
|
220
|
+
const oldest = nextItems.shift()!;
|
|
221
|
+
const oldId = leadingGovernanceId(oldest, policy);
|
|
222
|
+
if (oldId) collapsedIds.push(oldId);
|
|
223
|
+
}
|
|
224
|
+
const collapsed = collapsedIds.length > 0;
|
|
225
|
+
const nextHistory = mergeHistoryIds(historyIds, collapsedIds, policy);
|
|
226
|
+
|
|
227
|
+
const newBlock = buildChangeSummaryBlock(nextItems, nextHistory, relPath);
|
|
228
|
+
const transformed = source.replace(CHANGE_SUMMARY_BLOCK_RE, newBlock);
|
|
229
|
+
if (!dryRun && transformed !== source) {
|
|
230
|
+
await writeFileIfChanged(absPath, transformed);
|
|
231
|
+
}
|
|
232
|
+
return { recorded: true, collapsed };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function riskReminderNeeded(relPath: string, source: string, policy: CompassPolicy): boolean {
|
|
236
|
+
const segments = relPath.split("/").filter(Boolean);
|
|
237
|
+
const workspaceRel = getWorkspaceRelativeSegments(segments, policy).join("/");
|
|
238
|
+
const risk = detectRiskClass(relPath, workspaceRel, policy);
|
|
239
|
+
return risk === "medium" || risk === "high" || source.includes("<KEY_DECISIONS>");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function runCompassSummaryRecord(
|
|
243
|
+
input: ForgeCommandInput,
|
|
244
|
+
context: ForgeRuntimeContext,
|
|
245
|
+
): Promise<ForgeCommandResult<SummaryRecordResult>> {
|
|
246
|
+
const scanRoot = resolveCompassScanRoot(input, context);
|
|
247
|
+
const baseRoot = scanRoot ?? context.workspaceRoot;
|
|
248
|
+
const policy = resolveCompassPolicy(baseRoot, context.forgeRoot);
|
|
249
|
+
|
|
250
|
+
const id = input.flags["id"];
|
|
251
|
+
if (typeof id !== "string" || !isValidGovernanceId(id, policy)) {
|
|
252
|
+
context.logger.error(
|
|
253
|
+
`[compass.summary.record] --id is required and must be a governance ID (e.g. RFC-1095), got: ${String(id)}`,
|
|
254
|
+
);
|
|
255
|
+
return { exitCode: 1, summary: "invalid or missing --id" };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const rawFiles = input.flags["files"];
|
|
259
|
+
const files = Array.isArray(rawFiles)
|
|
260
|
+
? rawFiles
|
|
261
|
+
: typeof rawFiles === "string"
|
|
262
|
+
? rawFiles.split(/\s+/).filter(Boolean)
|
|
263
|
+
: [];
|
|
264
|
+
|
|
265
|
+
const rawText = input.flags["text"];
|
|
266
|
+
const text = typeof rawText === "string" && rawText.trim().length > 0 ? rawText.trim() : id;
|
|
267
|
+
|
|
268
|
+
const result: SummaryRecordResult = {
|
|
269
|
+
command: "compass.summary.record",
|
|
270
|
+
status: "pass",
|
|
271
|
+
recorded: [],
|
|
272
|
+
collapsed: [],
|
|
273
|
+
skipped: [],
|
|
274
|
+
keyDecisionsReminders: [],
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
for (const file of files) {
|
|
278
|
+
const absPath = resolve(baseRoot, file);
|
|
279
|
+
const relPath = relative(baseRoot, absPath).replace(/\\/g, "/");
|
|
280
|
+
if (!existsSync(absPath)) {
|
|
281
|
+
result.skipped.push({ file, reason: "unparseable" });
|
|
282
|
+
context.logger.warn(`[compass.summary.record] skipped ${file}: file not found`);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
const outcome = await recordSummaryItem(absPath, relPath, id, text, context.dryRun, policy);
|
|
287
|
+
if (outcome.skipReason) {
|
|
288
|
+
result.skipped.push({ file, reason: outcome.skipReason });
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (outcome.recorded) {
|
|
292
|
+
result.recorded.push(relPath);
|
|
293
|
+
if (outcome.collapsed) result.collapsed.push(relPath);
|
|
294
|
+
const source = await readFile(absPath, "utf8");
|
|
295
|
+
if (riskReminderNeeded(relPath, source, policy)) {
|
|
296
|
+
result.keyDecisionsReminders.push(relPath);
|
|
297
|
+
context.logger.warn(`[compass.summary.record] review KEY_DECISIONS in ${relPath}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} catch (err) {
|
|
301
|
+
result.skipped.push({ file, reason: "unparseable" });
|
|
302
|
+
context.logger.warn(
|
|
303
|
+
`[compass.summary.record] skipped ${file}: ${err instanceof Error ? err.message : String(err)}`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
data: result,
|
|
310
|
+
exitCode: 0,
|
|
311
|
+
summary: `[compass.summary.record] recorded=${result.recorded.length}, collapsed=${result.collapsed.length}, skipped=${result.skipped.length}`,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { migrateFile, type MigrateAction } from "../compass-migrate.ts";
|
|
7
|
+
import { runCompassMigrate } from "../compass-migrate-handler.ts";
|
|
8
|
+
import { resolveCompassPolicy, type CompassPolicy } from "../../policy.ts";
|
|
9
|
+
import type {
|
|
10
|
+
ForgeCommandInput,
|
|
11
|
+
ForgeCommandResult,
|
|
12
|
+
ForgeRuntimeContext,
|
|
13
|
+
} from "../../../../src/types.ts";
|
|
14
|
+
|
|
15
|
+
function dataOf<T>(result: ForgeCommandResult<T>): T {
|
|
16
|
+
if (!result.data) throw new Error("command returned no data payload");
|
|
17
|
+
return result.data;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function makeContext(workspaceRoot: string, dryRun = false): ForgeRuntimeContext {
|
|
21
|
+
return {
|
|
22
|
+
workspaceRoot,
|
|
23
|
+
site: undefined,
|
|
24
|
+
siteExplicit: false,
|
|
25
|
+
dryRun,
|
|
26
|
+
logger: { info: () => {}, warn: () => {}, error: () => {} },
|
|
27
|
+
} as unknown as ForgeRuntimeContext;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makeInput(flags: Record<string, unknown> = {}): ForgeCommandInput {
|
|
31
|
+
return { flags } as ForgeCommandInput;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Dynamic tags: literal CHANGE_SUMMARY blocks in this file would be matched
|
|
35
|
+
// by compass.summary.record itself and rewritten at commit time.
|
|
36
|
+
const CS_OPEN = "<" + "CHANGE_SUMMARY>";
|
|
37
|
+
const CS_CLOSE = "</" + "CHANGE_SUMMARY>";
|
|
38
|
+
const MC = (purpose: string) =>
|
|
39
|
+
`<MODULE_CONTRACT>\n<purpose>${purpose}</purpose>\n<non-goals>\n <item>Do not perform real validation — fixtures only.</item>\n</non-goals>\n</MODULE_CONTRACT>`;
|
|
40
|
+
const KD = (items: string[]) =>
|
|
41
|
+
`<KEY_DECISIONS>\n${items.map((i) => ` <item>${i}</item>`).join("\n")}\n</KEY_DECISIONS>`;
|
|
42
|
+
const CS = (items: string[], history?: string) =>
|
|
43
|
+
`${CS_OPEN}\n${items.map((i) => ` <item>${i}</item>`).join("\n")}\n${history ? ` <history>${history}</history>\n` : ""}${CS_CLOSE}`;
|
|
44
|
+
const wrap = (...blocks: string[]) => `/*\n${blocks.join("\n")}\n*/`;
|
|
45
|
+
|
|
46
|
+
const PURPOSE = "Validates mission widget fixtures for the compass migrate test suite.";
|
|
47
|
+
const KD_ITEM = "Fixtures live in a temp workspace so tests never touch the real repository.";
|
|
48
|
+
|
|
49
|
+
// Medium-risk: packages/<pkg>/src/components/** → workspace-relative src/components/** → layer component.
|
|
50
|
+
const MEDIUM_ROOT = "packages/fixture-pkg/src/components/mission-widget.ts";
|
|
51
|
+
const MEDIUM_WS = "src/components/mission-widget.ts";
|
|
52
|
+
// Low-risk: bare src/** → layer source.
|
|
53
|
+
const LOW_ROOT = "packages/fixture-pkg/src/mission-notes.ts";
|
|
54
|
+
const LOW_WS = "src/mission-notes.ts";
|
|
55
|
+
|
|
56
|
+
const BODY = Array.from({ length: 25 }, (_, i) => `export const fixtureLine${i} = ${i};`).join(
|
|
57
|
+
"\n",
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
let policy: CompassPolicy;
|
|
61
|
+
|
|
62
|
+
function migrate(source: string, root = MEDIUM_ROOT, ws = MEDIUM_WS) {
|
|
63
|
+
return migrateFile(`${source}\n\n${BODY}\n`, policy, root, ws);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("compass.migrate codemod (RFC-1097)", () => {
|
|
67
|
+
let tempDir: string;
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
tempDir = mkdtempSync(join(tmpdir(), "compass-migrate-test-"));
|
|
71
|
+
policy = resolveCompassPolicy(tempDir);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("no-header: file without any Compass block is skipped", () => {
|
|
79
|
+
const result = migrateFile(`export const x = 1;\n`, policy, MEDIUM_ROOT, MEDIUM_WS);
|
|
80
|
+
expect(result.actions).toEqual(["no-header"]);
|
|
81
|
+
expect(result.source).toBe("export const x = 1;\n");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("unparseable: open tag without close is skipped", () => {
|
|
85
|
+
const broken = `/*\n<MODULE_CONTRACT>\n<purpose>${PURPOSE}</purpose>\n`;
|
|
86
|
+
const result = migrateFile(broken, policy, MEDIUM_ROOT, MEDIUM_WS);
|
|
87
|
+
expect(result.actions).toEqual(["unparseable"]);
|
|
88
|
+
expect(result.source).toBe(broken);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("collapsed: 8 items keep newest 5, overflow IDs merge into sorted history, ID-less items drop", () => {
|
|
92
|
+
const items = [
|
|
93
|
+
"RFC-1001: first.",
|
|
94
|
+
"no-id item that must be dropped.",
|
|
95
|
+
"RFC-1002: second.",
|
|
96
|
+
"RFC-1003: third.",
|
|
97
|
+
"RFC-1004: fourth.",
|
|
98
|
+
"RFC-1005: fifth.",
|
|
99
|
+
"RFC-1006: sixth.",
|
|
100
|
+
"RFC-1007: seventh.",
|
|
101
|
+
"RFC-1008: eighth.",
|
|
102
|
+
];
|
|
103
|
+
const result = migrate(wrap(MC(PURPOSE), KD([KD_ITEM]), CS(items, "ADR-0001, RFC-0999")));
|
|
104
|
+
expect(result.actions).toContain("collapsed");
|
|
105
|
+
const csBlock = result.source.match(
|
|
106
|
+
new RegExp(
|
|
107
|
+
`${CS_OPEN.replace(/[<>]/g, "\\$&")}[\\s\\S]*?${CS_CLOSE.replace(/[<>/]/g, "\\$&")}`,
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
const kept = csBlock?.[0].match(/<item>([^<]+)<\/item>/g) ?? [];
|
|
111
|
+
// 5 newest CS items survive inside the block.
|
|
112
|
+
expect(kept.length).toBe(5);
|
|
113
|
+
expect(result.source).toContain("RFC-1008: eighth.");
|
|
114
|
+
expect(result.source).not.toContain("no-id item");
|
|
115
|
+
const historyMatch = result.source.match(/<history>([^<]+)<\/history>/);
|
|
116
|
+
expect(historyMatch, "collapsed IDs must land in history").toBeTruthy();
|
|
117
|
+
expect(historyMatch![1]).toBe("ADR-0001, RFC-0999, RFC-1001, RFC-1002, RFC-1003");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("stripped: MODULE_MAP, keywords, responsibilities and COMPASS_BLOCK anchors are removed", () => {
|
|
121
|
+
const mm = "<" + "MODULE_MAP" + ">\n <item>x</item>\n</" + "MODULE_MAP" + ">";
|
|
122
|
+
const kw = "<keywords>a, b</keywords>";
|
|
123
|
+
const resp = "<responsibilities>\n <item>y</item>\n</responsibilities>";
|
|
124
|
+
const anchorOpen = "<" + "COMPASS_BLOCK" + ">";
|
|
125
|
+
const anchorClose = "</" + "COMPASS_BLOCK" + ">";
|
|
126
|
+
const source = wrap(
|
|
127
|
+
anchorOpen,
|
|
128
|
+
MC(PURPOSE),
|
|
129
|
+
mm,
|
|
130
|
+
kw,
|
|
131
|
+
resp,
|
|
132
|
+
KD([KD_ITEM]),
|
|
133
|
+
CS(["RFC-1094: fixture."]),
|
|
134
|
+
anchorClose,
|
|
135
|
+
);
|
|
136
|
+
const result = migrate(source);
|
|
137
|
+
expect(result.actions).toContain("stripped");
|
|
138
|
+
expect(result.source).not.toContain("MODULE_MAP");
|
|
139
|
+
expect(result.source).not.toContain("keywords");
|
|
140
|
+
expect(result.source).not.toContain("responsibilities");
|
|
141
|
+
expect(result.source).not.toContain("COMPASS_BLOCK");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("reordered: KEY_DECISIONS after CHANGE_SUMMARY moves to canonical order", () => {
|
|
145
|
+
const result = migrate(wrap(MC(PURPOSE), CS(["RFC-1094: fixture."]), KD([KD_ITEM])));
|
|
146
|
+
expect(result.actions).toContain("reordered");
|
|
147
|
+
const mcIdx = result.source.indexOf("<MODULE_CONTRACT>");
|
|
148
|
+
const kdIdx = result.source.indexOf("<KEY_DECISIONS>");
|
|
149
|
+
const csIdx = result.source.indexOf(CS_OPEN);
|
|
150
|
+
expect(mcIdx).toBeLessThan(kdIdx);
|
|
151
|
+
expect(kdIdx).toBeLessThan(csIdx);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("seeded: medium-risk file with @ai-invariant comments gets real KEY_DECISIONS, ID prefix stripped", () => {
|
|
155
|
+
const source = `${wrap(MC(PURPOSE), CS(["RFC-1094: fixture."]))}\n// @ai-invariant RFC-0020: Widget tokens must stay deterministic.\n// @ai-invariant Widget tokens must stay deterministic.\n\n${BODY}\n`;
|
|
156
|
+
const result = migrateFile(source, policy, MEDIUM_ROOT, MEDIUM_WS);
|
|
157
|
+
expect(result.actions).toContain("seeded");
|
|
158
|
+
const kdMatch = result.source.match(/<KEY_DECISIONS>([\s\S]*?)<\/KEY_DECISIONS>/);
|
|
159
|
+
expect(kdMatch, "seeded file must carry a KEY_DECISIONS block").toBeTruthy();
|
|
160
|
+
expect(kdMatch![1]).toContain("Widget tokens must stay deterministic.");
|
|
161
|
+
// Deduped (two identical invariants → one item) and ID prefix stripped (KD-05).
|
|
162
|
+
expect(kdMatch![1]).not.toContain("RFC-0020");
|
|
163
|
+
expect((kdMatch![1].match(/<item>/g) ?? []).length).toBe(1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("todo: medium-risk file without @ai-invariant gets the TODO placeholder", () => {
|
|
167
|
+
const result = migrate(wrap(MC(PURPOSE), CS(["RFC-1094: fixture."])));
|
|
168
|
+
expect(result.actions).toContain("todo");
|
|
169
|
+
expect(result.source).toContain("TODO: record current design decisions");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("low-risk file without KEY_DECISIONS is not seeded", () => {
|
|
173
|
+
const result = migrateFile(
|
|
174
|
+
`${wrap(MC(PURPOSE), CS(["RFC-1094: fixture."]))}\n\n${BODY}\n`,
|
|
175
|
+
policy,
|
|
176
|
+
LOW_ROOT,
|
|
177
|
+
LOW_WS,
|
|
178
|
+
);
|
|
179
|
+
expect(result.actions).not.toContain("seeded");
|
|
180
|
+
expect(result.actions).not.toContain("todo");
|
|
181
|
+
expect(result.source).not.toContain("<KEY_DECISIONS>");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("purpose-flagged: boilerplate purpose is reported without rewriting", () => {
|
|
185
|
+
const result = migrate(
|
|
186
|
+
wrap(
|
|
187
|
+
MC("This file provides various reusable capabilities for the wider workspace ecosystem."),
|
|
188
|
+
KD([KD_ITEM]),
|
|
189
|
+
CS(["RFC-1094: fixture."]),
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
expect(result.actions).toEqual(["purpose-flagged"]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("unchanged: fully compliant v2 file reports unchanged and is idempotent", () => {
|
|
196
|
+
const v2 = wrap(MC(PURPOSE), KD([KD_ITEM]), CS(["RFC-1094: fixture."], "RFC-0348"));
|
|
197
|
+
const first = migrate(v2);
|
|
198
|
+
expect(first.actions).toEqual(["unchanged"]);
|
|
199
|
+
const second = migrateFile(first.source, policy, MEDIUM_ROOT, MEDIUM_WS);
|
|
200
|
+
expect(second.actions).toEqual(["unchanged"]);
|
|
201
|
+
expect(second.source).toBe(first.source);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("idempotent: a migrated file migrates to unchanged on the second pass", () => {
|
|
205
|
+
const items = Array.from({ length: 8 }, (_, i) => `RFC-100${i}: item ${i}.`);
|
|
206
|
+
const first = migrate(wrap(MC(PURPOSE), CS(items)));
|
|
207
|
+
expect(first.actions.length).toBeGreaterThan(0);
|
|
208
|
+
const second = migrateFile(first.source, policy, MEDIUM_ROOT, MEDIUM_WS);
|
|
209
|
+
expect(
|
|
210
|
+
second.actions,
|
|
211
|
+
`second pass must be a no-op, got ${JSON.stringify(second.actions)}`,
|
|
212
|
+
).toEqual(["unchanged"]);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("runCompassMigrate handler (RFC-1097)", () => {
|
|
217
|
+
let tempDir: string;
|
|
218
|
+
|
|
219
|
+
function git(...args: string[]): void {
|
|
220
|
+
execFileSync("git", args, { cwd: tempDir, stdio: "ignore", timeout: 10000 });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function writeFixture(relPath: string, header: string): void {
|
|
224
|
+
const abs = join(tempDir, relPath);
|
|
225
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
226
|
+
writeFileSync(abs, `${header}\n\n${BODY}\n`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
beforeEach(() => {
|
|
230
|
+
tempDir = mkdtempSync(join(tmpdir(), "compass-migrate-handler-"));
|
|
231
|
+
policy = resolveCompassPolicy(tempDir);
|
|
232
|
+
git("init", "-q");
|
|
233
|
+
git("config", "user.email", "test@test.com");
|
|
234
|
+
git("config", "user.name", "Test");
|
|
235
|
+
git("commit", "-q", "--allow-empty", "-m", "init");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
afterEach(() => {
|
|
239
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("refuses a dirty git tree without --force and lists dirty paths", async () => {
|
|
243
|
+
writeFixture(MEDIUM_ROOT, wrap(MC(PURPOSE), CS(["RFC-1094: fixture."])));
|
|
244
|
+
|
|
245
|
+
const result = await runCompassMigrate(makeInput(), makeContext(tempDir));
|
|
246
|
+
expect(result.exitCode).toBe(1);
|
|
247
|
+
expect(result.summary).toContain("dirty");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("--force overrides the dirty-tree refusal", async () => {
|
|
251
|
+
writeFixture(MEDIUM_ROOT, wrap(MC(PURPOSE), CS(["RFC-1094: fixture."])));
|
|
252
|
+
|
|
253
|
+
const result = await runCompassMigrate(makeInput({ force: true }), makeContext(tempDir));
|
|
254
|
+
expect(result.exitCode).toBe(0);
|
|
255
|
+
const data = dataOf(result);
|
|
256
|
+
expect(data.scanned).toBeGreaterThan(0);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("dry-run reports actions without writing", async () => {
|
|
260
|
+
const items = Array.from({ length: 8 }, (_, i) => `RFC-100${i}: item ${i}.`);
|
|
261
|
+
writeFixture(MEDIUM_ROOT, wrap(MC(PURPOSE), KD([KD_ITEM]), CS(items)));
|
|
262
|
+
git("add", "-A");
|
|
263
|
+
git("commit", "-q", "-m", "fixture");
|
|
264
|
+
|
|
265
|
+
const before = (await import("node:fs/promises")).readFile;
|
|
266
|
+
const original = await before(join(tempDir, MEDIUM_ROOT), "utf8");
|
|
267
|
+
const result = await runCompassMigrate(makeInput({ "dry-run": true }), makeContext(tempDir));
|
|
268
|
+
expect(result.exitCode).toBe(0);
|
|
269
|
+
const data = dataOf(result);
|
|
270
|
+
const entry = data.files.find((f) => f.path === MEDIUM_ROOT);
|
|
271
|
+
expect(entry?.actions).toContain("collapsed");
|
|
272
|
+
const after = await before(join(tempDir, MEDIUM_ROOT), "utf8");
|
|
273
|
+
expect(after, "dry-run must not write").toBe(original);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("--files bypasses scan roots and processes the explicit list", async () => {
|
|
277
|
+
const outside = "custom-dir/widget.ts";
|
|
278
|
+
writeFixture(outside, wrap(MC(PURPOSE), CS(["RFC-1094: fixture."])));
|
|
279
|
+
git("add", "-A");
|
|
280
|
+
git("commit", "-q", "-m", "fixture");
|
|
281
|
+
|
|
282
|
+
const result = await runCompassMigrate(makeInput({ files: [outside] }), makeContext(tempDir));
|
|
283
|
+
expect(result.exitCode).toBe(0);
|
|
284
|
+
const data = dataOf(result);
|
|
285
|
+
expect(data.files).toHaveLength(1);
|
|
286
|
+
expect(data.files[0]!.path).toBe(outside);
|
|
287
|
+
const actions: MigrateAction[] = data.files[0]!.actions;
|
|
288
|
+
// custom-dir/widget.ts is low-risk (no layer match) → no seeding, v1 file is already clean.
|
|
289
|
+
expect(actions).toEqual(["unchanged"]);
|
|
290
|
+
});
|
|
291
|
+
});
|