@dommaker/harness 1.7.0 → 1.8.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/CHANGELOG.md +44 -0
- package/bin/harness.js +17 -1
- package/dist/agents/index.d.ts +6 -2
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +6 -16
- package/dist/agents/index.js.map +1 -1
- package/dist/cli/commands/__tests__/gate-decision.d.ts +18 -0
- package/dist/cli/commands/__tests__/gate-decision.d.ts.map +1 -0
- package/dist/cli/commands/__tests__/gate-decision.js +26 -0
- package/dist/cli/commands/__tests__/gate-decision.js.map +1 -0
- package/dist/cli/commands/acceptance.d.ts.map +1 -1
- package/dist/cli/commands/acceptance.js +17 -28
- package/dist/cli/commands/acceptance.js.map +1 -1
- package/dist/cli/commands/check.d.ts +19 -3
- package/dist/cli/commands/check.d.ts.map +1 -1
- package/dist/cli/commands/check.js +36 -22
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/command.d.ts +0 -1
- package/dist/cli/commands/command.d.ts.map +1 -1
- package/dist/cli/commands/command.js +8 -6
- package/dist/cli/commands/command.js.map +1 -1
- package/dist/cli/commands/constraints-retire.d.ts.map +1 -1
- package/dist/cli/commands/constraints-retire.js +36 -57
- package/dist/cli/commands/constraints-retire.js.map +1 -1
- package/dist/cli/commands/contract.d.ts.map +1 -1
- package/dist/cli/commands/contract.js +36 -42
- package/dist/cli/commands/contract.js.map +1 -1
- package/dist/cli/commands/definitions.d.ts +2 -0
- package/dist/cli/commands/definitions.d.ts.map +1 -1
- package/dist/cli/commands/definitions.js +11 -6
- package/dist/cli/commands/definitions.js.map +1 -1
- package/dist/cli/commands/failure.d.ts.map +1 -1
- package/dist/cli/commands/failure.js +16 -2
- package/dist/cli/commands/failure.js.map +1 -1
- package/dist/cli/commands/init.d.ts +14 -8
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +172 -364
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/knowledge-view.d.ts +90 -0
- package/dist/cli/commands/knowledge-view.d.ts.map +1 -0
- package/dist/cli/commands/knowledge-view.js +198 -0
- package/dist/cli/commands/knowledge-view.js.map +1 -0
- package/dist/cli/commands/knowledge.d.ts +137 -52
- package/dist/cli/commands/knowledge.d.ts.map +1 -1
- package/dist/cli/commands/knowledge.js +534 -400
- package/dist/cli/commands/knowledge.js.map +1 -1
- package/dist/cli/commands/passes-gate.d.ts +4 -1
- package/dist/cli/commands/passes-gate.d.ts.map +1 -1
- package/dist/cli/commands/passes-gate.js +11 -2
- package/dist/cli/commands/passes-gate.js.map +1 -1
- package/dist/cli/commands/performance.d.ts.map +1 -1
- package/dist/cli/commands/performance.js +19 -31
- package/dist/cli/commands/performance.js.map +1 -1
- package/dist/cli/commands/report.d.ts.map +1 -1
- package/dist/cli/commands/report.js +2 -1
- package/dist/cli/commands/report.js.map +1 -1
- package/dist/cli/commands/review.d.ts.map +1 -1
- package/dist/cli/commands/review.js +13 -25
- package/dist/cli/commands/review.js.map +1 -1
- package/dist/cli/commands/scaffold-templates.d.ts +73 -0
- package/dist/cli/commands/scaffold-templates.d.ts.map +1 -0
- package/dist/cli/commands/scaffold-templates.js +424 -0
- package/dist/cli/commands/scaffold-templates.js.map +1 -0
- package/dist/cli/commands/scaffold.d.ts +105 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -0
- package/dist/cli/commands/scaffold.js +263 -0
- package/dist/cli/commands/scaffold.js.map +1 -0
- package/dist/cli/commands/security.d.ts.map +1 -1
- package/dist/cli/commands/security.js +22 -24
- package/dist/cli/commands/security.js.map +1 -1
- package/dist/cli/commands/spec-baseline-check.d.ts +43 -0
- package/dist/cli/commands/spec-baseline-check.d.ts.map +1 -1
- package/dist/cli/commands/spec-baseline-check.js +109 -34
- package/dist/cli/commands/spec-baseline-check.js.map +1 -1
- package/dist/cli/commands/status.d.ts +8 -0
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +5 -7
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/sync-docs/context-syncer.d.ts +21 -2
- package/dist/cli/commands/sync-docs/context-syncer.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/context-syncer.js +112 -28
- package/dist/cli/commands/sync-docs/context-syncer.js.map +1 -1
- package/dist/cli/commands/sync-docs/index.d.ts +1 -1
- package/dist/cli/commands/sync-docs/index.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/index.js +86 -27
- package/dist/cli/commands/sync-docs/index.js.map +1 -1
- package/dist/cli/commands/sync-docs/project-reader.d.ts +24 -1
- package/dist/cli/commands/sync-docs/project-reader.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/project-reader.js +33 -7
- package/dist/cli/commands/sync-docs/project-reader.js.map +1 -1
- package/dist/cli/commands/validate.d.ts +0 -16
- package/dist/cli/commands/validate.d.ts.map +1 -1
- package/dist/cli/commands/validate.js +2 -97
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/gate-command.d.ts +37 -0
- package/dist/cli/gate-command.d.ts.map +1 -0
- package/dist/cli/gate-command.js +64 -0
- package/dist/cli/gate-command.js.map +1 -0
- package/dist/cli/state-io.d.ts +34 -0
- package/dist/cli/state-io.d.ts.map +1 -0
- package/dist/cli/state-io.js +71 -0
- package/dist/cli/state-io.js.map +1 -0
- package/dist/completion-checkers/index.d.ts +1 -1
- package/dist/completion-checkers/index.d.ts.map +1 -1
- package/dist/completion-checkers/index.js +0 -15
- package/dist/completion-checkers/index.js.map +1 -1
- package/dist/context/knowledge-injector.d.ts +4 -2
- package/dist/context/knowledge-injector.d.ts.map +1 -1
- package/dist/context/knowledge-injector.js +13 -6
- package/dist/context/knowledge-injector.js.map +1 -1
- package/dist/core/constraints/agent-prompt-renderer.js +2 -2
- package/dist/core/constraints/agent-prompt-renderer.js.map +1 -1
- package/dist/core/constraints/capabilities-reconcile.d.ts +20 -0
- package/dist/core/constraints/capabilities-reconcile.d.ts.map +1 -1
- package/dist/core/constraints/capabilities-reconcile.js +26 -4
- package/dist/core/constraints/capabilities-reconcile.js.map +1 -1
- package/dist/core/constraints/check-cache.d.ts +1 -1
- package/dist/core/constraints/check-cache.js +1 -1
- package/dist/core/constraints/checker.d.ts +8 -3
- package/dist/core/constraints/checker.d.ts.map +1 -1
- package/dist/core/constraints/checker.js +30 -19
- package/dist/core/constraints/checker.js.map +1 -1
- package/dist/core/constraints/checkers/capability-sync.d.ts +3 -1
- package/dist/core/constraints/checkers/capability-sync.d.ts.map +1 -1
- package/dist/core/constraints/checkers/capability-sync.js +17 -24
- package/dist/core/constraints/checkers/capability-sync.js.map +1 -1
- package/dist/core/constraints/checkers/context-doc-sync.d.ts +1 -1
- package/dist/core/constraints/checkers/context-doc-sync.d.ts.map +1 -1
- package/dist/core/constraints/checkers/context-doc-sync.js +9 -6
- package/dist/core/constraints/checkers/context-doc-sync.js.map +1 -1
- package/dist/core/constraints/checkers/docs-freshness.d.ts.map +1 -1
- package/dist/core/constraints/checkers/docs-freshness.js +24 -30
- package/dist/core/constraints/checkers/docs-freshness.js.map +1 -1
- package/dist/core/constraints/checkers/governance-presence.d.ts.map +1 -1
- package/dist/core/constraints/checkers/governance-presence.js +10 -4
- package/dist/core/constraints/checkers/governance-presence.js.map +1 -1
- package/dist/core/constraints/checkers/types.d.ts +10 -6
- package/dist/core/constraints/checkers/types.d.ts.map +1 -1
- package/dist/core/constraints/checkers/types.js +7 -3
- package/dist/core/constraints/checkers/types.js.map +1 -1
- package/dist/core/constraints/context-builder.d.ts +11 -10
- package/dist/core/constraints/context-builder.d.ts.map +1 -1
- package/dist/core/constraints/context-builder.js +31 -27
- package/dist/core/constraints/context-builder.js.map +1 -1
- package/dist/core/constraints/context-reconcile.d.ts +75 -0
- package/dist/core/constraints/context-reconcile.d.ts.map +1 -0
- package/dist/core/constraints/context-reconcile.js +161 -0
- package/dist/core/constraints/context-reconcile.js.map +1 -0
- package/dist/core/constraints/doc-freshness/runner.d.ts +7 -0
- package/dist/core/constraints/doc-freshness/runner.d.ts.map +1 -1
- package/dist/core/constraints/doc-freshness/runner.js +8 -3
- package/dist/core/constraints/doc-freshness/runner.js.map +1 -1
- package/dist/core/constraints/git-evidence.js +1 -1
- package/dist/core/constraints/git-evidence.js.map +1 -1
- package/dist/core/constraints/injection-drift.d.ts +7 -2
- package/dist/core/constraints/injection-drift.d.ts.map +1 -1
- package/dist/core/constraints/injection-drift.js +13 -7
- package/dist/core/constraints/injection-drift.js.map +1 -1
- package/dist/core/constraints/run-env.d.ts +92 -0
- package/dist/core/constraints/run-env.d.ts.map +1 -0
- package/dist/core/constraints/run-env.js +175 -0
- package/dist/core/constraints/run-env.js.map +1 -0
- package/dist/core/constraints/triggers.d.ts +22 -0
- package/dist/core/constraints/triggers.d.ts.map +1 -0
- package/dist/core/constraints/triggers.js +31 -0
- package/dist/core/constraints/triggers.js.map +1 -0
- package/dist/core/effective-constraints.d.ts +13 -4
- package/dist/core/effective-constraints.d.ts.map +1 -1
- package/dist/core/effective-constraints.js +16 -6
- package/dist/core/effective-constraints.js.map +1 -1
- package/dist/core/effective-set.d.ts +8 -15
- package/dist/core/effective-set.d.ts.map +1 -1
- package/dist/core/effective-set.js +7 -12
- package/dist/core/effective-set.js.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/project-config-loader.d.ts +39 -17
- package/dist/core/project-config-loader.d.ts.map +1 -1
- package/dist/core/project-config-loader.js +51 -122
- package/dist/core/project-config-loader.js.map +1 -1
- package/dist/core/validators/check-handlers/http.d.ts +2 -2
- package/dist/core/validators/check-handlers/http.d.ts.map +1 -1
- package/dist/core/validators/check-handlers/http.js +12 -7
- package/dist/core/validators/check-handlers/http.js.map +1 -1
- package/dist/failure/classifier.d.ts +1 -1
- package/dist/failure/classifier.d.ts.map +1 -1
- package/dist/failure/classifier.js +8 -8
- package/dist/failure/classifier.js.map +1 -1
- package/dist/failure/index.d.ts +2 -1
- package/dist/failure/index.d.ts.map +1 -1
- package/dist/failure/index.js +9 -17
- package/dist/failure/index.js.map +1 -1
- package/dist/failure/recorder.d.ts +1 -1
- package/dist/failure/recorder.d.ts.map +1 -1
- package/dist/gates/checker-gate.d.ts +2 -1
- package/dist/gates/checker-gate.d.ts.map +1 -1
- package/dist/gates/checker-gate.js +3 -2
- package/dist/gates/checker-gate.js.map +1 -1
- package/dist/gates/command.d.ts +10 -4
- package/dist/gates/command.d.ts.map +1 -1
- package/dist/gates/command.js +36 -62
- package/dist/gates/command.js.map +1 -1
- package/dist/gates/contract.d.ts.map +1 -1
- package/dist/gates/contract.js +0 -4
- package/dist/gates/contract.js.map +1 -1
- package/dist/gates/definitions.d.ts +2 -1
- package/dist/gates/definitions.d.ts.map +1 -1
- package/dist/gates/definitions.js +6 -5
- package/dist/gates/definitions.js.map +1 -1
- package/dist/gates/index.d.ts +0 -4
- package/dist/gates/index.d.ts.map +1 -1
- package/dist/gates/index.js +1 -6
- package/dist/gates/index.js.map +1 -1
- package/dist/gates/performance.d.ts.map +1 -1
- package/dist/gates/performance.js +0 -4
- package/dist/gates/performance.js.map +1 -1
- package/dist/gates/runner.d.ts +1 -1
- package/dist/gates/runner.js +1 -1
- package/dist/gates/security.d.ts +1 -1
- package/dist/gates/security.d.ts.map +1 -1
- package/dist/gates/security.js +5 -6
- package/dist/gates/security.js.map +1 -1
- package/dist/gates/types.d.ts +14 -17
- package/dist/gates/types.d.ts.map +1 -1
- package/dist/gates/types.js.map +1 -1
- package/dist/hooks/bootstrap.d.ts +6 -4
- package/dist/hooks/bootstrap.d.ts.map +1 -1
- package/dist/hooks/bootstrap.js +25 -13
- package/dist/hooks/bootstrap.js.map +1 -1
- package/dist/hooks/config.d.ts +6 -2
- package/dist/hooks/config.d.ts.map +1 -1
- package/dist/hooks/config.js +5 -1
- package/dist/hooks/config.js.map +1 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +0 -15
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/pipeline.d.ts +19 -1
- package/dist/hooks/pipeline.d.ts.map +1 -1
- package/dist/hooks/pipeline.js +42 -4
- package/dist/hooks/pipeline.js.map +1 -1
- package/dist/hooks/registry.d.ts +11 -8
- package/dist/hooks/registry.d.ts.map +1 -1
- package/dist/hooks/registry.js +22 -7
- package/dist/hooks/registry.js.map +1 -1
- package/dist/hooks/types.d.ts +25 -7
- package/dist/hooks/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/knowledge/audit-scoring.d.ts +87 -0
- package/dist/knowledge/audit-scoring.d.ts.map +1 -0
- package/dist/knowledge/audit-scoring.js +470 -0
- package/dist/knowledge/audit-scoring.js.map +1 -0
- package/dist/knowledge/audit.d.ts +11 -59
- package/dist/knowledge/audit.d.ts.map +1 -1
- package/dist/knowledge/audit.js +47 -522
- package/dist/knowledge/audit.js.map +1 -1
- package/dist/knowledge/import.d.ts.map +1 -1
- package/dist/knowledge/import.js +2 -0
- package/dist/knowledge/import.js.map +1 -1
- package/dist/knowledge/index-generator.d.ts.map +1 -1
- package/dist/knowledge/index-generator.js +4 -5
- package/dist/knowledge/index-generator.js.map +1 -1
- package/dist/knowledge/index.d.ts +3 -2
- package/dist/knowledge/index.d.ts.map +1 -1
- package/dist/knowledge/index.js +3 -16
- package/dist/knowledge/index.js.map +1 -1
- package/dist/knowledge/ingest.js +1 -1
- package/dist/knowledge/ingest.js.map +1 -1
- package/dist/knowledge/lifecycle.d.ts.map +1 -1
- package/dist/knowledge/lifecycle.js +4 -2
- package/dist/knowledge/lifecycle.js.map +1 -1
- package/dist/knowledge/migration.d.ts +3 -0
- package/dist/knowledge/migration.d.ts.map +1 -1
- package/dist/knowledge/migration.js +8 -2
- package/dist/knowledge/migration.js.map +1 -1
- package/dist/knowledge/query.d.ts +6 -0
- package/dist/knowledge/query.d.ts.map +1 -1
- package/dist/knowledge/query.js +8 -2
- package/dist/knowledge/query.js.map +1 -1
- package/dist/knowledge/reference-tracker.d.ts.map +1 -1
- package/dist/knowledge/reference-tracker.js +4 -6
- package/dist/knowledge/reference-tracker.js.map +1 -1
- package/dist/knowledge/store.d.ts +22 -16
- package/dist/knowledge/store.d.ts.map +1 -1
- package/dist/knowledge/store.js +53 -12
- package/dist/knowledge/store.js.map +1 -1
- package/dist/knowledge/tree-walker.d.ts +19 -0
- package/dist/knowledge/tree-walker.d.ts.map +1 -0
- package/dist/knowledge/tree-walker.js +30 -0
- package/dist/knowledge/tree-walker.js.map +1 -0
- package/dist/knowledge/types.d.ts +16 -0
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/monitoring/traces.d.ts +5 -3
- package/dist/monitoring/traces.d.ts.map +1 -1
- package/dist/monitoring/traces.js +19 -5
- package/dist/monitoring/traces.js.map +1 -1
- package/dist/test-setup/jsonl-fake-fs.d.ts +30 -0
- package/dist/test-setup/jsonl-fake-fs.d.ts.map +1 -0
- package/dist/test-setup/jsonl-fake-fs.js +49 -0
- package/dist/test-setup/jsonl-fake-fs.js.map +1 -0
- package/dist/tools/index.d.ts +4 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +7 -15
- package/dist/tools/index.js.map +1 -1
- package/dist/types/checkpoint.d.ts +5 -0
- package/dist/types/checkpoint.d.ts.map +1 -1
- package/dist/types/project-config.d.ts +18 -0
- package/dist/types/project-config.d.ts.map +1 -1
- package/dist/types/trace.d.ts +13 -4
- package/dist/types/trace.d.ts.map +1 -1
- package/dist/types/trace.js +4 -3
- package/dist/types/trace.js.map +1 -1
- package/dist/utils/exec.d.ts +4 -27
- package/dist/utils/exec.d.ts.map +1 -1
- package/dist/utils/exec.js +4 -94
- package/dist/utils/exec.js.map +1 -1
- package/dist/utils/jsonl.d.ts +17 -7
- package/dist/utils/jsonl.d.ts.map +1 -1
- package/dist/utils/jsonl.js +144 -17
- package/dist/utils/jsonl.js.map +1 -1
- package/dist/utils/numeric-flag.d.ts +44 -0
- package/dist/utils/numeric-flag.d.ts.map +1 -0
- package/dist/utils/numeric-flag.js +62 -0
- package/dist/utils/numeric-flag.js.map +1 -0
- package/package.json +1 -1
- package/src/CONTEXT.md +5 -2
- package/src/__tests__/acceptance-gate.test.ts +7 -4
- package/src/__tests__/checker-extra.test.ts +3 -2
- package/src/__tests__/checker.test.ts +4 -3
- package/src/__tests__/checkpoint.test.ts +26 -9
- package/src/__tests__/context-files-resolution.test.ts +29 -15
- package/src/__tests__/contract-gate.test.ts +19 -40
- package/src/__tests__/error-classifier.test.ts +1 -1
- package/src/__tests__/exec.test.ts +45 -107
- package/src/__tests__/failure-recorder.test.ts +7 -5
- package/src/__tests__/hooks-pipeline.test.ts +34 -47
- package/src/__tests__/layering.test.ts +217 -25
- package/src/__tests__/passes-gate.test.ts +9 -4
- package/src/__tests__/performance-gate.test.ts +3 -15
- package/src/__tests__/project-config-loader-extra.test.ts +31 -57
- package/src/__tests__/project-config-loader.test.ts +6 -3
- package/src/__tests__/public-exports.test.ts +0 -1
- package/src/__tests__/public-type-surface.test.ts +5 -4
- package/src/__tests__/public-value-surface.test.ts +182 -0
- package/src/__tests__/review-gate.test.ts +4 -6
- package/src/__tests__/security-gate.test.ts +5 -20
- package/src/__tests__/skip-semantics.test.ts +5 -3
- package/src/__tests__/spec-validator.test.ts +3 -2
- package/src/__tests__/sub-barrels-explicit.test.ts +87 -0
- package/src/__tests__/test-fixture-hygiene.test.ts +53 -0
- package/src/__tests__/trace-analyzer-extra.test.ts +5 -3
- package/src/__tests__/trace-analyzer.test.ts +5 -3
- package/src/__tests__/trace-collector.test.ts +5 -3
- package/src/agents/index.ts +13 -2
- package/src/cli/__tests__/gate-command.test.ts +162 -0
- package/src/cli/__tests__/state-io.test.ts +50 -0
- package/src/cli/commands/CONTEXT.md +30 -5
- package/src/cli/commands/__tests__/acceptance.test.ts +24 -25
- package/src/cli/commands/__tests__/bin-exit-mapping.test.ts +15 -0
- package/src/cli/commands/__tests__/check-drift.test.ts +42 -1
- package/src/cli/commands/__tests__/check-read-count.test.ts +170 -0
- package/src/cli/commands/__tests__/check-skip-output.test.ts +4 -1
- package/src/cli/commands/__tests__/check.test.ts +63 -44
- package/src/cli/commands/__tests__/command-gate-dead-options.test.ts +60 -0
- package/src/cli/commands/__tests__/command.test.ts +33 -17
- package/src/cli/commands/__tests__/constraints-retire.test.ts +52 -0
- package/src/cli/commands/__tests__/context-syncer.test.ts +141 -0
- package/src/cli/commands/__tests__/contract.test.ts +20 -20
- package/src/cli/commands/__tests__/failure.test.ts +5 -4
- package/src/cli/commands/__tests__/gate-decision.ts +30 -0
- package/src/cli/commands/__tests__/init-dead-options.test.ts +50 -0
- package/src/cli/commands/__tests__/init-governance-io.test.ts +139 -0
- package/src/cli/commands/__tests__/init-injection.test.ts +3 -3
- package/src/cli/commands/__tests__/init-ondisk.test.ts +867 -0
- package/src/cli/commands/__tests__/init.test.ts +82 -6
- package/src/cli/commands/__tests__/knowledge-audit-threshold.test.ts +146 -0
- package/src/cli/commands/__tests__/knowledge-flywheel-consistency.test.ts +1 -1
- package/src/cli/commands/__tests__/knowledge-tone-mapping.test.ts +244 -0
- package/src/cli/commands/__tests__/knowledge-view.test.ts +839 -0
- package/src/cli/commands/__tests__/knowledge.test.ts +33 -15
- package/src/cli/commands/__tests__/numeric-flag-assembly.test.ts +368 -0
- package/src/cli/commands/__tests__/performance.test.ts +19 -21
- package/src/cli/commands/__tests__/posteval-plan.test.ts +5 -3
- package/src/cli/commands/__tests__/pre-push-hook.test.ts +181 -0
- package/src/cli/commands/__tests__/project-path-anchoring.test.ts +95 -2
- package/src/cli/commands/__tests__/project-path-convention.test.ts +103 -25
- package/src/cli/commands/__tests__/registry.test.ts +2 -0
- package/src/cli/commands/__tests__/report.test.ts +23 -1
- package/src/cli/commands/__tests__/review.test.ts +10 -17
- package/src/cli/commands/__tests__/scaffold.test.ts +505 -0
- package/src/cli/commands/__tests__/security-default-scan.test.ts +75 -0
- package/src/cli/commands/__tests__/security.test.ts +21 -24
- package/src/cli/commands/__tests__/spec-baseline-check.test.ts +3 -2
- package/src/cli/commands/__tests__/spec-baseline-index-cap.test.ts +131 -0
- package/src/cli/commands/__tests__/spec-baseline-read-count.test.ts +325 -0
- package/src/cli/commands/__tests__/status-extra.test.ts +25 -8
- package/src/cli/commands/__tests__/status.test.ts +46 -18
- package/src/cli/commands/__tests__/sync-docs-agents.test.ts +19 -4
- package/src/cli/commands/__tests__/sync-docs-context-drift.test.ts +232 -0
- package/src/cli/commands/__tests__/sync-docs-module-order.test.ts +111 -0
- package/src/cli/commands/__tests__/sync-docs-read-count.test.ts +192 -0
- package/src/cli/commands/__tests__/sync-docs.test.ts +3 -2
- package/src/cli/commands/__tests__/validate.test.ts +1 -48
- package/src/cli/commands/acceptance.ts +24 -29
- package/src/cli/commands/check.ts +60 -35
- package/src/cli/commands/command.ts +9 -8
- package/src/cli/commands/constraints-retire.ts +57 -70
- package/src/cli/commands/contract.ts +44 -47
- package/src/cli/commands/definitions.ts +12 -6
- package/src/cli/commands/failure.ts +16 -3
- package/src/cli/commands/init.ts +242 -389
- package/src/cli/commands/knowledge-view.ts +224 -0
- package/src/cli/commands/knowledge.ts +643 -448
- package/src/cli/commands/passes-gate.ts +12 -3
- package/src/cli/commands/performance.ts +31 -38
- package/src/cli/commands/report.ts +3 -2
- package/src/cli/commands/review.ts +22 -28
- package/src/cli/commands/scaffold-templates.ts +406 -0
- package/src/cli/commands/scaffold.ts +302 -0
- package/src/cli/commands/security.ts +27 -23
- package/src/cli/commands/spec-baseline-check.ts +158 -33
- package/src/cli/commands/status.ts +12 -7
- package/src/cli/commands/sync-docs/__tests__/context-export-surface.test.ts +102 -0
- package/src/cli/commands/sync-docs/__tests__/module-descriptions.test.ts +196 -0
- package/src/cli/commands/sync-docs/context-syncer.ts +137 -31
- package/src/cli/commands/sync-docs/index.ts +103 -28
- package/src/cli/commands/sync-docs/project-reader.ts +51 -7
- package/src/cli/commands/validate.ts +1 -104
- package/src/cli/gate-command.ts +87 -0
- package/src/cli/state-io.ts +53 -0
- package/src/completion-checkers/index.ts +10 -1
- package/src/context/CONTEXT.md +2 -1
- package/src/context/__tests__/knowledge-injector.test.ts +27 -0
- package/src/context/__tests__/session-manager.test.ts +19 -12
- package/src/context/knowledge-injector.ts +14 -7
- package/src/core/CONTEXT.md +10 -7
- package/src/core/__tests__/effective-set.test.ts +36 -60
- package/src/core/constraints/__tests__/context-reconcile.test.ts +246 -0
- package/src/core/constraints/__tests__/run-env.test.ts +377 -0
- package/src/core/constraints/__tests__/triggers.test.ts +70 -0
- package/src/core/constraints/agent-prompt-renderer.ts +1 -1
- package/src/core/constraints/capabilities-reconcile.ts +28 -4
- package/src/core/constraints/check-cache.ts +1 -1
- package/src/core/constraints/checker.ts +30 -14
- package/src/core/constraints/checkers/__tests__/context-doc-sync.test.ts +8 -3
- package/src/core/constraints/checkers/__tests__/governance-presence.test.ts +16 -7
- package/src/core/constraints/checkers/__tests__/no-hardcoded-credentials.test.ts +2 -1
- package/src/core/constraints/checkers/capability-sync.ts +27 -28
- package/src/core/constraints/checkers/context-doc-sync.ts +9 -7
- package/src/core/constraints/checkers/docs-freshness.ts +28 -33
- package/src/core/constraints/checkers/governance-presence.ts +10 -6
- package/src/core/constraints/checkers/types.ts +14 -8
- package/src/core/constraints/context-builder.ts +35 -26
- package/src/core/constraints/context-reconcile.ts +200 -0
- package/src/core/constraints/doc-freshness/__tests__/runner.test.ts +25 -0
- package/src/core/constraints/doc-freshness/runner.ts +18 -4
- package/src/core/constraints/git-evidence.ts +1 -1
- package/src/core/constraints/injection-drift.ts +19 -8
- package/src/core/constraints/run-env.ts +207 -0
- package/src/core/constraints/triggers.ts +33 -0
- package/src/core/effective-constraints.ts +17 -6
- package/src/core/effective-set.ts +8 -24
- package/src/core/index.ts +2 -0
- package/src/core/project-config-loader.ts +54 -102
- package/src/core/validators/__tests__/http-retry-sleep.test.ts +231 -0
- package/src/core/validators/check-handlers/http.ts +18 -7
- package/src/failure/CONTEXT.md +2 -1
- package/src/failure/classifier.ts +2 -2
- package/src/failure/index.ts +14 -2
- package/src/failure/recorder.ts +1 -1
- package/src/gates/CONTEXT.md +13 -7
- package/src/gates/__tests__/checker-gate.test.ts +1 -1
- package/src/gates/__tests__/command-single-predicate.test.ts +135 -0
- package/src/gates/__tests__/command.test.ts +8 -15
- package/src/gates/__tests__/contract.test.ts +5 -5
- package/src/gates/__tests__/gate-interface.test.ts +0 -17
- package/src/gates/__tests__/performance.test.ts +0 -13
- package/src/gates/__tests__/review.test.ts +0 -1
- package/src/gates/__tests__/runner.test.ts +1 -1
- package/src/gates/__tests__/security-default-scan.test.ts +97 -0
- package/src/gates/__tests__/security.test.ts +0 -13
- package/src/gates/checker-gate.ts +3 -2
- package/src/gates/command.ts +51 -74
- package/src/gates/contract.ts +0 -5
- package/src/gates/definitions.ts +6 -5
- package/src/gates/index.ts +0 -5
- package/src/gates/performance.ts +1 -6
- package/src/gates/runner.ts +1 -1
- package/src/gates/security.ts +9 -11
- package/src/gates/types.ts +20 -23
- package/src/hooks/CONTEXT.md +10 -9
- package/src/hooks/__tests__/bootstrap.test.ts +29 -6
- package/src/hooks/__tests__/pipeline.test.ts +133 -17
- package/src/hooks/__tests__/registry.test.ts +51 -3
- package/src/hooks/bootstrap.ts +35 -15
- package/src/hooks/config.ts +6 -2
- package/src/hooks/index.ts +10 -1
- package/src/hooks/pipeline.ts +51 -6
- package/src/hooks/registry.ts +31 -12
- package/src/hooks/types.ts +26 -7
- package/src/index.ts +1 -2
- package/src/knowledge/CONTEXT.md +14 -7
- package/src/knowledge/__tests__/audit-labels.test.ts +8 -5
- package/src/knowledge/__tests__/audit-scoring.test.ts +230 -0
- package/src/knowledge/__tests__/audit-write-count.test.ts +131 -0
- package/src/knowledge/__tests__/audit.test.ts +291 -208
- package/src/knowledge/__tests__/doctor.test.ts +3 -2
- package/src/knowledge/__tests__/ingest.test.ts +3 -2
- package/src/knowledge/__tests__/lifecycle.test.ts +57 -2
- package/src/knowledge/__tests__/migration.test.ts +35 -6
- package/src/knowledge/__tests__/query.test.ts +3 -2
- package/src/knowledge/__tests__/reference-tracker.test.ts +23 -2
- package/src/knowledge/__tests__/store.test.ts +149 -4
- package/src/knowledge/__tests__/tree-walker.test.ts +132 -0
- package/src/knowledge/audit-scoring.ts +589 -0
- package/src/knowledge/audit.ts +55 -599
- package/src/knowledge/import.ts +2 -0
- package/src/knowledge/index-generator.ts +4 -6
- package/src/knowledge/index.ts +27 -2
- package/src/knowledge/ingest.ts +1 -1
- package/src/knowledge/lifecycle.ts +5 -2
- package/src/knowledge/migration.ts +8 -2
- package/src/knowledge/query.ts +8 -1
- package/src/knowledge/reference-tracker.ts +5 -7
- package/src/knowledge/store.ts +75 -16
- package/src/knowledge/tree-walker.ts +29 -0
- package/src/knowledge/types.ts +19 -0
- package/src/monitoring/CONTEXT.md +5 -2
- package/src/monitoring/__tests__/context-tracker.test.ts +16 -8
- package/src/monitoring/__tests__/trace-file-anchoring.test.ts +80 -0
- package/src/monitoring/traces.ts +19 -5
- package/src/release/CONTEXT.md +1 -1
- package/src/release/__tests__/integrity.test.ts +21 -12
- package/src/test-setup/__tests__/jsonl-fake-fs.test.ts +98 -0
- package/src/test-setup/jsonl-fake-fs.ts +59 -0
- package/src/tools/index.ts +5 -1
- package/src/types/checkpoint.ts +6 -0
- package/src/types/project-config.ts +21 -0
- package/src/types/trace.ts +14 -4
- package/src/utils/__tests__/file-walk.test.ts +3 -2
- package/src/utils/__tests__/jsonl-bounded-read.test.ts +106 -0
- package/src/utils/__tests__/jsonl-skip-disposition.test.ts +10 -6
- package/src/utils/__tests__/jsonl.test.ts +61 -14
- package/src/utils/__tests__/numeric-flag.test.ts +80 -0
- package/src/utils/__tests__/package-version.test.ts +2 -2
- package/src/utils/exec.ts +4 -64
- package/src/utils/jsonl.ts +159 -20
- package/src/utils/numeric-flag.ts +64 -0
- package/dist/failure/types.d.ts +0 -9
- package/dist/failure/types.d.ts.map +0 -1
- package/dist/failure/types.js +0 -25
- package/dist/failure/types.js.map +0 -1
- package/dist/gates/effective-gates.d.ts +0 -30
- package/dist/gates/effective-gates.d.ts.map +0 -1
- package/dist/gates/effective-gates.js +0 -81
- package/dist/gates/effective-gates.js.map +0 -1
- package/src/failure/types.ts +0 -9
- package/src/gates/__tests__/effective-gates.test.ts +0 -77
- package/src/gates/effective-gates.ts +0 -99
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init 落盘集成测试(harness#132)
|
|
3
|
+
*
|
|
4
|
+
* init.test.ts 把 fs/promises 整体 mock 掉,只能看到「调过 writeFile」,看不到
|
|
5
|
+
* 真写出了什么内容。本文件不 mock 任何 IO:在临时目录里真跑两遍 init——
|
|
6
|
+
* 第一遍断言 9 个受管文件的落盘字节,第二遍(文件已在场且被用户改过)断言
|
|
7
|
+
* 9 站点各自的第三态、用户内容一字节不动、以及「打印片段 == 真正落盘的内容」。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from 'fs/promises';
|
|
11
|
+
import * as os from 'os';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as yaml from 'js-yaml';
|
|
14
|
+
import { init, type InitOptions } from '../init';
|
|
15
|
+
import { captureIO, type CapturingIO } from '../../command-contract';
|
|
16
|
+
|
|
17
|
+
jest.mock('chalk', () => ({
|
|
18
|
+
blue: jest.fn((s: string) => s),
|
|
19
|
+
yellow: jest.fn((s: string) => s),
|
|
20
|
+
green: jest.fn((s: string) => s),
|
|
21
|
+
gray: jest.fn((s: string) => s),
|
|
22
|
+
red: jest.fn((s: string) => s),
|
|
23
|
+
cyan: jest.fn((s: string) => s),
|
|
24
|
+
bold: jest.fn((s: string) => s),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const INIT_OPTIONS = { preset: 'standard' as const, governance: 'standard' as const };
|
|
28
|
+
|
|
29
|
+
/** 版本号随发布漂移,冻结时归一化 */
|
|
30
|
+
function normalize(text: string, root: string): string {
|
|
31
|
+
return text.split(root).join('<P>').replace(/\(v[\d.]+\)/g, '(v*)');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function makeProject(withGit = true): Promise<string> {
|
|
35
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'harness-scaffold-'));
|
|
36
|
+
await fs.mkdir(path.join(root, 'src'));
|
|
37
|
+
await fs.writeFile(path.join(root, 'src', 'index.ts'), 'export const x = 1;\n');
|
|
38
|
+
if (withGit) await fs.mkdir(path.join(root, '.git'));
|
|
39
|
+
return root;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** expected 里的行按顺序出现在 lines 中(中间可夹片段正文等其他行) */
|
|
43
|
+
function expectOrderedSubsequence(lines: string[], expected: string[]): void {
|
|
44
|
+
let cursor = -1;
|
|
45
|
+
for (const want of expected) {
|
|
46
|
+
const found = lines.indexOf(want, cursor + 1);
|
|
47
|
+
if (found <= cursor) throw new Error(`第三态缺失或顺序错乱: ${want}\n实际输出:\n${lines.join('\n')}`);
|
|
48
|
+
cursor = found;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* .git/hooks/pre-push 的字节级冻结基线(harness#144;正本:`scaffold-templates.renderPrePushHook`)。
|
|
54
|
+
* 三道决定都在这里被钉住:整仓全量(`check` 不带 `--staged`)+ validate、任一失败 exit 1、
|
|
55
|
+
* 无增量解析/无逃生机制。改模板必须同步改这份基线——「同步」这个动作正是拦住门禁被削弱的地方。
|
|
56
|
+
*/
|
|
57
|
+
const PRE_PUSH_HOOK_BYTES = `#!/bin/sh
|
|
58
|
+
# Harness pre-push hook
|
|
59
|
+
|
|
60
|
+
echo "🔍 Running harness pre-push checks (whole repo)..."
|
|
61
|
+
|
|
62
|
+
# 分工:pre-commit 查暂存的(增量、快反馈),pre-push 查整仓的(全量、兜底)。
|
|
63
|
+
# 同一次改动跑两遍是设计使然——这道拦的是 git commit --no-verify 绕出去的内容。
|
|
64
|
+
# 本地 hook 只是自检与提醒,真正的门禁在服务端 CI;逃生口只有 git 原生 push --no-verify。
|
|
65
|
+
# 不解析 pre-push 从 stdin 收到的 ref 清单,子进程一律不给它读(< /dev/null)。
|
|
66
|
+
npx @dommaker/harness check < /dev/null
|
|
67
|
+
if [ $? -ne 0 ]; then
|
|
68
|
+
echo "❌ Iron law check failed"
|
|
69
|
+
exit 1
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
npx @dommaker/harness validate < /dev/null
|
|
73
|
+
if [ $? -ne 0 ]; then
|
|
74
|
+
echo "❌ Validate failed"
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
echo "✅ All pre-push checks passed"
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* src/CONTEXT.md 骨架的字节级冻结基线(harness#158;正本:`scaffold-templates.renderContextDoc('src')`)。
|
|
83
|
+
* #150 把 init 与 sync-docs 归一到单一正本后,「改正本无测试报警」成了新缺口(复审 M3:
|
|
84
|
+
* 给正本加一行 → 全绿)。改骨架必须同步改这份基线——「同步」这个动作正是拦住骨架被静改的地方。
|
|
85
|
+
*/
|
|
86
|
+
const CONTEXT_MD_BYTES = `# src
|
|
87
|
+
|
|
88
|
+
> 此文件描述 src 目录的职责和上下文
|
|
89
|
+
> 请阅读本目录的源代码,然后填写以下各节。
|
|
90
|
+
> 如果使用 AI 编码助手,将本文件内容作为 prompt 请求它分析并填写。
|
|
91
|
+
|
|
92
|
+
## 职责
|
|
93
|
+
|
|
94
|
+
<!-- 本目录的核心职责是什么 -->
|
|
95
|
+
|
|
96
|
+
## 核心导出
|
|
97
|
+
|
|
98
|
+
<!-- 本目录对外暴露的主要模块/函数 -->
|
|
99
|
+
|
|
100
|
+
## 依赖关系
|
|
101
|
+
|
|
102
|
+
<!-- 本目录依赖哪些其他模块,谁依赖本目录 -->
|
|
103
|
+
|
|
104
|
+
## 注意事项
|
|
105
|
+
|
|
106
|
+
<!-- 开发时需要注意的约束或约定 -->
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
describe('init 真落盘(无 IO mock)', () => {
|
|
110
|
+
const roots: string[] = [];
|
|
111
|
+
|
|
112
|
+
afterAll(async () => {
|
|
113
|
+
await Promise.all(roots.map(root => fs.rm(root, { recursive: true, force: true })));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('第一遍:受管文件逐字节落盘,整屏输出逐字冻结', async () => {
|
|
117
|
+
const root = await makeProject();
|
|
118
|
+
roots.push(root);
|
|
119
|
+
const io = captureIO();
|
|
120
|
+
|
|
121
|
+
expect(await init({ ...INIT_OPTIONS, projectPath: root }, io)).toEqual({ kind: 'ok' });
|
|
122
|
+
|
|
123
|
+
// 冻结的整屏 = scaffold 9 站点 + 治理段 writer 的落盘提示。#149 前 io 在
|
|
124
|
+
// setupGovernance 两个调用点被丢,治理 writer 的提示逃到 process.stdout、不进捕获面
|
|
125
|
+
// (本文件因此曾缺这一行);io 贯通后它回到捕获面,真机输出的字节不变。
|
|
126
|
+
expect(normalize(io.outText(), root)).toBe(`🚀 初始化 harness 配置...
|
|
127
|
+
配置目录: <P>/.harness
|
|
128
|
+
预设: standard
|
|
129
|
+
治理级别: standard
|
|
130
|
+
✅ 已创建配置文件: <P>/.harness/config.yml (v*)
|
|
131
|
+
✅ 已创建示例检查点文件: <P>/.harness/checkpoints.yml
|
|
132
|
+
✅ 已创建 Resolutions 文件: <P>/.harness/resolutions.json
|
|
133
|
+
✅ 已创建自定义约束示例: custom-constraints.yml
|
|
134
|
+
✅ 已创建 .git/hooks/pre-commit
|
|
135
|
+
✅ 已创建 .git/hooks/pre-push
|
|
136
|
+
✅ 已创建 .github/workflows/harness-check.yml
|
|
137
|
+
|
|
138
|
+
📋 设置治理文件...
|
|
139
|
+
✅ 已创建 CHANGELOG.md
|
|
140
|
+
✅ 已创建 AGENTS.md 并写入治理契约 PRESERVE:governance 段 (v*)
|
|
141
|
+
✅ 已创建 src/CONTEXT.md
|
|
142
|
+
治理检查已由 harness-check.yml 覆盖,跳过创建 harness-governance.yml
|
|
143
|
+
|
|
144
|
+
✅ harness 初始化完成!
|
|
145
|
+
|
|
146
|
+
下一步:
|
|
147
|
+
1. 编辑 .harness/config.yml 自定义配置
|
|
148
|
+
2. 编辑 .harness/custom-constraints.yml 添加项目约束
|
|
149
|
+
3. 正常开发:每次 git commit 查暂存的(增量快反馈),每次 git push 查整仓的(全量兜底)——重复是设计使然
|
|
150
|
+
4. 运行 harness status 查看状态
|
|
151
|
+
|
|
152
|
+
💡 提示: 使用 harness init --print-snippets 查看配置代码片段
|
|
153
|
+
`);
|
|
154
|
+
|
|
155
|
+
const read = (rel: string) => fs.readFile(path.join(root, rel), 'utf-8');
|
|
156
|
+
|
|
157
|
+
const hook = await read('.git/hooks/pre-commit');
|
|
158
|
+
expect(hook.startsWith('#!/bin/sh\n# Harness pre-commit hook\n')).toBe(true);
|
|
159
|
+
expect(hook).toContain('echo "🔍 Running harness checks..."');
|
|
160
|
+
expect(hook).toContain("grep -E 'plans/.*\\.md$|\\.plan\\.md$'");
|
|
161
|
+
expect(hook.endsWith('echo "✅ All checks passed"\n')).toBe(true);
|
|
162
|
+
|
|
163
|
+
// 第 9 站点(#144):字节级冻结,改模板必须同步改这份基线(同 GITLAB_PLAIN 手法)
|
|
164
|
+
expect(await read('.git/hooks/pre-push')).toBe(PRE_PUSH_HOOK_BYTES);
|
|
165
|
+
|
|
166
|
+
expect(await read('.github/workflows/harness-check.yml')).toContain('name: Harness Check');
|
|
167
|
+
expect(await read('.harness/custom-constraints.yml')).toContain('custom_constraints:');
|
|
168
|
+
expect(await read('.harness/checkpoints.yml')).toContain('id: build-success');
|
|
169
|
+
expect(JSON.parse(await read('.harness/resolutions.json')).capability_sync).toBeDefined();
|
|
170
|
+
expect(await read('CHANGELOG.md')).toContain('Keep a Changelog');
|
|
171
|
+
// src/CONTEXT.md 骨架(#158):字节级冻结,同 PRE_PUSH_HOOK_BYTES 手法
|
|
172
|
+
expect(await read('src/CONTEXT.md')).toBe(CONTEXT_MD_BYTES);
|
|
173
|
+
|
|
174
|
+
for (const hookFile of ['.git/hooks/pre-commit', '.git/hooks/pre-push']) {
|
|
175
|
+
const stat = await fs.stat(path.join(root, hookFile));
|
|
176
|
+
expect(stat.mode & 0o111).toBeTruthy();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('第二遍(harness 自己的文件在场):8 站点走冲突分支,打印片段即磁盘正文', async () => {
|
|
181
|
+
const root = await makeProject();
|
|
182
|
+
roots.push(root);
|
|
183
|
+
await init({ ...INIT_OPTIONS, projectPath: root }, captureIO());
|
|
184
|
+
|
|
185
|
+
const io = captureIO();
|
|
186
|
+
await init({ ...INIT_OPTIONS, projectPath: root }, io);
|
|
187
|
+
const lines = io.outLines();
|
|
188
|
+
|
|
189
|
+
// 顺序 = init 的落盘顺序;harness-governance.yml(第九站)因既有 workflow 覆盖而跳过
|
|
190
|
+
expectOrderedSubsequence(lines, [
|
|
191
|
+
'checkpoints.yml 已存在,跳过',
|
|
192
|
+
'resolutions.json 已存在,跳过',
|
|
193
|
+
'custom-constraints.yml 已存在',
|
|
194
|
+
'⚠️ .git/hooks/pre-commit 已存在',
|
|
195
|
+
'💡 请手动添加以下内容到文件末尾:',
|
|
196
|
+
'⚠️ .git/hooks/pre-push 已存在',
|
|
197
|
+
'💡 请手动添加以下内容到文件末尾:',
|
|
198
|
+
'⚠️ 检测到已存在的 CI 配置:',
|
|
199
|
+
' - .github/workflows/harness-check.yml',
|
|
200
|
+
'💡 请手动添加以下内容到 jobs 中:',
|
|
201
|
+
'CHANGELOG.md 已存在',
|
|
202
|
+
'src/CONTEXT.md 已存在',
|
|
203
|
+
'治理检查已由 harness-check.yml 覆盖,跳过创建 harness-governance.yml',
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
// 同源不变量(#103 判据):冲突分支打印的就是磁盘上那份正文
|
|
207
|
+
const hook = await fs.readFile(path.join(root, '.git/hooks/pre-commit'), 'utf-8');
|
|
208
|
+
expect(hook.startsWith('#!/bin/sh\n# Harness pre-commit hook\n')).toBe(true);
|
|
209
|
+
expect(io.outText()).toContain(hook.replace('#!/bin/sh\n# Harness pre-commit hook\n', ''));
|
|
210
|
+
const prePush = await fs.readFile(path.join(root, '.git/hooks/pre-push'), 'utf-8');
|
|
211
|
+
expect(prePush).toBe(PRE_PUSH_HOOK_BYTES);
|
|
212
|
+
expect(io.outText()).toContain(prePush.replace('#!/bin/sh\n# Harness pre-push hook\n', ''));
|
|
213
|
+
const workflow = await fs.readFile(path.join(root, '.github/workflows/harness-check.yml'), 'utf-8');
|
|
214
|
+
expect(io.outText()).toContain(workflow);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('第三遍(用户已接管这些文件):一字节都不改', async () => {
|
|
218
|
+
const root = await makeProject();
|
|
219
|
+
roots.push(root);
|
|
220
|
+
await init({ ...INIT_OPTIONS, projectPath: root }, captureIO());
|
|
221
|
+
|
|
222
|
+
const userFiles = {
|
|
223
|
+
'.git/hooks/pre-commit': '#!/bin/sh\n# 我自己的钩子\n',
|
|
224
|
+
'.git/hooks/pre-push': '#!/bin/sh\n# 我自己的推送钩子\n',
|
|
225
|
+
'.github/workflows/harness-check.yml': 'name: 我自己的流水线\n',
|
|
226
|
+
'.harness/custom-constraints.yml': 'custom_constraints: {}\n',
|
|
227
|
+
'.harness/checkpoints.yml': 'checkpoints: []\n',
|
|
228
|
+
'.harness/resolutions.json': '{}\n',
|
|
229
|
+
'CHANGELOG.md': '# 我的更新日志\n',
|
|
230
|
+
'src/CONTEXT.md': '# src\n\n我写的\n',
|
|
231
|
+
} as const;
|
|
232
|
+
for (const [rel, content] of Object.entries(userFiles)) {
|
|
233
|
+
await fs.writeFile(path.join(root, rel), content);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await init({ ...INIT_OPTIONS, projectPath: root }, captureIO());
|
|
237
|
+
|
|
238
|
+
for (const [rel, content] of Object.entries(userFiles)) {
|
|
239
|
+
expect(await fs.readFile(path.join(root, rel), 'utf-8')).toBe(content);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('治理 CI 站点:github 下既有 workflow 不覆盖治理命令时自建,第二遍告知已存在', async () => {
|
|
244
|
+
const root = await makeProject(false);
|
|
245
|
+
roots.push(root);
|
|
246
|
+
// 用户自带的 workflow 不含治理命令:harness-check 站点走 manual(只打印不落盘),
|
|
247
|
+
// 治理命令无任何覆盖 → 自建 harness-governance.yml(本站点唯一会真落盘的场景)
|
|
248
|
+
await fs.mkdir(path.join(root, '.github/workflows'), { recursive: true });
|
|
249
|
+
await fs.writeFile(path.join(root, '.github/workflows/ci.yml'), 'name: 我自己的流水线\n');
|
|
250
|
+
const first = captureIO();
|
|
251
|
+
|
|
252
|
+
await init({ ...INIT_OPTIONS, projectPath: root }, first);
|
|
253
|
+
expect(normalize(first.outText(), root)).toContain('✅ 已创建 .github/workflows/harness-governance.yml');
|
|
254
|
+
const governance = await fs.readFile(path.join(root, '.github/workflows/harness-governance.yml'), 'utf-8');
|
|
255
|
+
expect(governance).toContain('name: Harness Governance');
|
|
256
|
+
expect(governance).toContain('npx @dommaker/harness passes-gate');
|
|
257
|
+
|
|
258
|
+
const second = captureIO();
|
|
259
|
+
await init({ ...INIT_OPTIONS, projectPath: root }, second);
|
|
260
|
+
expect(second.outLines()).toContain('harness-governance.yml 已存在');
|
|
261
|
+
expect(await fs.readFile(path.join(root, '.github/workflows/harness-governance.yml'), 'utf-8')).toBe(governance);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('治理 CI 站点:--ci none(含 --no-github-actions 别名)带 -g 也一律不建(harness#156 裁决 F1)', async () => {
|
|
265
|
+
// 本用例原是「无 GH Actions 旗帜时自建」——钉的是 #143 有意保留的旧边界;
|
|
266
|
+
// #156 裁决 F1 走「none = 不创建任何 CI 文件」,旧断言整体翻面
|
|
267
|
+
const root = await makeProject(false);
|
|
268
|
+
roots.push(root);
|
|
269
|
+
const io = captureIO();
|
|
270
|
+
|
|
271
|
+
await init({ ...INIT_OPTIONS, githubActions: false, projectPath: root }, io);
|
|
272
|
+
|
|
273
|
+
expect(io.outLines()).toContain('⚠️ --no-github-actions 已废弃,请改用 --ci none');
|
|
274
|
+
expect(io.outText()).not.toContain('harness-governance.yml');
|
|
275
|
+
expect(await exists(path.join(root, '.github'))).toBe(false);
|
|
276
|
+
expect(await exists(path.join(root, '.gitlab-ci.yml'))).toBe(false);
|
|
277
|
+
// 治理文档面照常:CHANGELOG / 约束段 / CONTEXT.md 不受 CI 豁免影响
|
|
278
|
+
expect(await exists(path.join(root, 'CHANGELOG.md'))).toBe(true);
|
|
279
|
+
expect(await exists(path.join(root, 'AGENTS.md'))).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('无 .git 与无源码目录时仍按原措辞告知,不写任何文件', async () => {
|
|
283
|
+
const root = await makeProject(false);
|
|
284
|
+
roots.push(root);
|
|
285
|
+
await fs.rm(path.join(root, 'src'), { recursive: true });
|
|
286
|
+
|
|
287
|
+
const io = captureIO();
|
|
288
|
+
await init({ ...INIT_OPTIONS, governance: undefined, projectPath: root }, io);
|
|
289
|
+
expect(io.outLines()).toContain('⚠️ 未检测到 Git 仓库,跳过 Git hooks');
|
|
290
|
+
|
|
291
|
+
const withDocsDir = await makeProject(false);
|
|
292
|
+
roots.push(withDocsDir);
|
|
293
|
+
const io2 = captureIO();
|
|
294
|
+
await init({ ...INIT_OPTIONS, projectPath: withDocsDir }, io2);
|
|
295
|
+
expect(io2.outText()).not.toContain('不存在,跳过 CONTEXT.md');
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// ── CI 平台维度(harness#143):落盘正文的字节级正本 ─────────────────────
|
|
300
|
+
|
|
301
|
+
const RULES = ` rules:
|
|
302
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
303
|
+
- if: $CI_COMMIT_BRANCH == "main"
|
|
304
|
+
- if: $CI_COMMIT_BRANCH == "master"
|
|
305
|
+
`;
|
|
306
|
+
|
|
307
|
+
const GITLAB_PLAIN = `# 由 harness init 生成 —— 以下 harness-* 任务是 harness 的 CI 接线
|
|
308
|
+
# 门禁真正生效还需在 GitLab 侧开分支保护:pipeline 成功才允许合并
|
|
309
|
+
|
|
310
|
+
image: node:20
|
|
311
|
+
|
|
312
|
+
stages:
|
|
313
|
+
- test
|
|
314
|
+
|
|
315
|
+
harness-check:
|
|
316
|
+
stage: test
|
|
317
|
+
script:
|
|
318
|
+
- npm ci
|
|
319
|
+
- npx @dommaker/harness check
|
|
320
|
+
- npx @dommaker/harness validate
|
|
321
|
+
- npx @dommaker/harness passes-gate
|
|
322
|
+
${RULES}`;
|
|
323
|
+
|
|
324
|
+
const GITLAB_GOVERNED = GITLAB_PLAIN + `
|
|
325
|
+
harness-governance:
|
|
326
|
+
stage: test
|
|
327
|
+
script:
|
|
328
|
+
- npm ci
|
|
329
|
+
- npx @dommaker/harness check
|
|
330
|
+
- npx @dommaker/harness passes-gate
|
|
331
|
+
${RULES}
|
|
332
|
+
harness-docs-freshness:
|
|
333
|
+
stage: test
|
|
334
|
+
script:
|
|
335
|
+
- npm ci
|
|
336
|
+
- npx @dommaker/harness sync-docs --check
|
|
337
|
+
${RULES} allow_failure: true
|
|
338
|
+
`;
|
|
339
|
+
|
|
340
|
+
/** GitLab 站点的 harness-check 任务正文(冲突分支打印的 job 片段即此份) */
|
|
341
|
+
const GITLAB_CHECK_JOB = GITLAB_PLAIN.slice(GITLAB_PLAIN.indexOf('harness-check:'));
|
|
342
|
+
|
|
343
|
+
const CI_OPTIONS = { preset: 'standard' as const };
|
|
344
|
+
|
|
345
|
+
async function exists(rel: string): Promise<boolean> {
|
|
346
|
+
try {
|
|
347
|
+
await fs.access(rel);
|
|
348
|
+
return true;
|
|
349
|
+
} catch {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
describe('init --ci 平台维度(harness#143,真落盘)', () => {
|
|
355
|
+
const roots: string[] = [];
|
|
356
|
+
|
|
357
|
+
afterAll(async () => {
|
|
358
|
+
await Promise.all(roots.map(root => fs.rm(root, { recursive: true, force: true })));
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('--ci gitlab:逐字节落盘 .gitlab-ci.yml,不写 GH workflow,平台持久化进 config.yml', async () => {
|
|
362
|
+
const root = await makeProject();
|
|
363
|
+
roots.push(root);
|
|
364
|
+
const io = captureIO();
|
|
365
|
+
|
|
366
|
+
expect(await init({ ...CI_OPTIONS, ci: 'gitlab', projectPath: root }, io)).toEqual({ kind: 'ok' });
|
|
367
|
+
|
|
368
|
+
expect(await fs.readFile(path.join(root, '.gitlab-ci.yml'), 'utf-8')).toBe(GITLAB_PLAIN);
|
|
369
|
+
expect(await exists(path.join(root, '.github'))).toBe(false);
|
|
370
|
+
expect(io.outLines()).toContain('✅ 已创建 .gitlab-ci.yml');
|
|
371
|
+
expect(io.outLines()).toContain('CI 平台: gitlab');
|
|
372
|
+
expect(await fs.readFile(path.join(root, '.harness', 'config.yml'), 'utf-8')).toContain(
|
|
373
|
+
'ci:\n platform: gitlab',
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('迁移方向:gitlab 已持久化后再 --ci github → config.yml 的 ci 键消失,回到 GitHub 形状', async () => {
|
|
378
|
+
const root = await makeProject();
|
|
379
|
+
roots.push(root);
|
|
380
|
+
await init({ ...CI_OPTIONS, ci: 'gitlab', projectPath: root }, captureIO());
|
|
381
|
+
expect(await fs.readFile(path.join(root, '.harness', 'config.yml'), 'utf-8')).toContain(
|
|
382
|
+
'ci:\n platform: gitlab',
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const io = captureIO();
|
|
386
|
+
expect(await init({ ...CI_OPTIONS, ci: 'github', projectPath: root }, io)).toEqual({ kind: 'ok' });
|
|
387
|
+
|
|
388
|
+
// github 是缺省平台,非默认平台才写键(旧配置零迁移)——显式 --ci github 必须把
|
|
389
|
+
// 已持久化的 ci: 键抹掉,而不是留下 platform: github(harness#158 补钉,复审 M9 留口)
|
|
390
|
+
const config = yaml.load(await fs.readFile(path.join(root, '.harness', 'config.yml'), 'utf-8')) as Record<string, unknown>;
|
|
391
|
+
expect('ci' in config).toBe(false);
|
|
392
|
+
expect(io.outText()).not.toContain('CI 平台:');
|
|
393
|
+
expect(await fs.readFile(path.join(root, '.github/workflows/harness-check.yml'), 'utf-8'))
|
|
394
|
+
.toContain('name: Harness Check');
|
|
395
|
+
// 旧的 gitlab 站点是用户文件,不删不动
|
|
396
|
+
expect(await fs.readFile(path.join(root, '.gitlab-ci.yml'), 'utf-8')).toBe(GITLAB_PLAIN);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('第二趟(.gitlab-ci.yml 已在场):manual 态,用户内容一字节不动,打印的 job 片段即落盘正文的一部分', async () => {
|
|
400
|
+
const root = await makeProject();
|
|
401
|
+
roots.push(root);
|
|
402
|
+
await init({ ...CI_OPTIONS, ci: 'gitlab', projectPath: root }, captureIO());
|
|
403
|
+
const gitlabFile = path.join(root, '.gitlab-ci.yml');
|
|
404
|
+
const userContent = '# 我自己的流水线\n\ntest:\n script:\n - echo hi\n';
|
|
405
|
+
await fs.writeFile(gitlabFile, userContent);
|
|
406
|
+
|
|
407
|
+
const io = captureIO();
|
|
408
|
+
await init({ ...CI_OPTIONS, ci: 'gitlab', projectPath: root }, io);
|
|
409
|
+
|
|
410
|
+
expect(io.outLines()).toContain('⚠️ .gitlab-ci.yml 已存在');
|
|
411
|
+
expect(io.outLines()).toContain('💡 请手动添加以下内容到文件中:');
|
|
412
|
+
expect(io.outText()).toContain(GITLAB_CHECK_JOB);
|
|
413
|
+
expect(await fs.readFile(gitlabFile, 'utf-8')).toBe(userContent);
|
|
414
|
+
expect(await exists(path.join(root, '.github'))).toBe(false);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('--ci gitlab -g standard:治理任务并入同一份 .gitlab-ci.yml,docs 新鲜度 allow_failure: true', async () => {
|
|
418
|
+
const root = await makeProject();
|
|
419
|
+
roots.push(root);
|
|
420
|
+
const io = captureIO();
|
|
421
|
+
|
|
422
|
+
await init({ ...INIT_OPTIONS, ci: 'gitlab', projectPath: root }, io);
|
|
423
|
+
|
|
424
|
+
expect(await fs.readFile(path.join(root, '.gitlab-ci.yml'), 'utf-8')).toBe(GITLAB_GOVERNED);
|
|
425
|
+
expect(await exists(path.join(root, '.github'))).toBe(false);
|
|
426
|
+
expect(io.outText()).not.toContain('harness-governance.yml');
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('平台持久化:裸 init 与 --print-snippets 都按 config.yml 的 ci.platform 出面', async () => {
|
|
430
|
+
const root = await makeProject();
|
|
431
|
+
roots.push(root);
|
|
432
|
+
await init({ ...CI_OPTIONS, ci: 'gitlab', projectPath: root }, captureIO());
|
|
433
|
+
await fs.rm(path.join(root, '.gitlab-ci.yml'));
|
|
434
|
+
|
|
435
|
+
const bare = captureIO();
|
|
436
|
+
await init({ ...CI_OPTIONS, projectPath: root }, bare);
|
|
437
|
+
expect(bare.outLines()).toContain('✅ 已创建 .gitlab-ci.yml');
|
|
438
|
+
expect(bare.outText()).not.toContain('harness-check.yml');
|
|
439
|
+
expect(await fs.readFile(path.join(root, '.gitlab-ci.yml'), 'utf-8')).toBe(GITLAB_PLAIN);
|
|
440
|
+
// 裸 init 重写 config.yml 时不得丢掉已持久化的平台
|
|
441
|
+
expect(await fs.readFile(path.join(root, '.harness', 'config.yml'), 'utf-8')).toContain(
|
|
442
|
+
'ci:\n platform: gitlab',
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const snippets = captureIO();
|
|
446
|
+
await init({ ...CI_OPTIONS, printSnippets: true, projectPath: root }, snippets);
|
|
447
|
+
expect(snippets.outLines()).toContain('GitLab CI:');
|
|
448
|
+
expect(snippets.outText()).toContain(GITLAB_CHECK_JOB);
|
|
449
|
+
expect(snippets.outText()).not.toContain('GitHub Actions:');
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('--ci none 不落任何 CI 文件;--no-github-actions 与之等价并打 deprecation warning', async () => {
|
|
453
|
+
const noneRoot = await makeProject();
|
|
454
|
+
roots.push(noneRoot);
|
|
455
|
+
const none = captureIO();
|
|
456
|
+
await init({ ...CI_OPTIONS, ci: 'none', projectPath: noneRoot }, none);
|
|
457
|
+
expect(await exists(path.join(noneRoot, '.github'))).toBe(false);
|
|
458
|
+
expect(await exists(path.join(noneRoot, '.gitlab-ci.yml'))).toBe(false);
|
|
459
|
+
expect(none.outText()).not.toContain('已废弃');
|
|
460
|
+
expect(await fs.readFile(path.join(noneRoot, '.harness', 'config.yml'), 'utf-8')).toContain(
|
|
461
|
+
'ci:\n platform: none',
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
const legacyRoot = await makeProject();
|
|
465
|
+
roots.push(legacyRoot);
|
|
466
|
+
const legacy = captureIO();
|
|
467
|
+
await init({ ...CI_OPTIONS, githubActions: false, projectPath: legacyRoot }, legacy);
|
|
468
|
+
expect(legacy.outLines()).toContain('⚠️ --no-github-actions 已废弃,请改用 --ci none');
|
|
469
|
+
expect(await exists(path.join(legacyRoot, '.github'))).toBe(false);
|
|
470
|
+
expect(await fs.readFile(path.join(legacyRoot, '.harness', 'config.yml'), 'utf-8')).toContain(
|
|
471
|
+
'ci:\n platform: none',
|
|
472
|
+
);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('--ci gitlab 与 --no-github-actions 同时给出:用法错误且什么都不落盘', async () => {
|
|
476
|
+
const root = await makeProject();
|
|
477
|
+
roots.push(root);
|
|
478
|
+
const io = captureIO();
|
|
479
|
+
|
|
480
|
+
const result = await init({ ...CI_OPTIONS, ci: 'gitlab', githubActions: false, projectPath: root }, io);
|
|
481
|
+
|
|
482
|
+
expect(result).toEqual({
|
|
483
|
+
kind: 'usage-error',
|
|
484
|
+
reason: expect.stringContaining('--no-github-actions'),
|
|
485
|
+
});
|
|
486
|
+
expect(io.errText()).toContain('用法错误');
|
|
487
|
+
expect(await exists(path.join(root, '.gitlab-ci.yml'))).toBe(false);
|
|
488
|
+
expect(await exists(path.join(root, '.harness', 'config.yml'))).toBe(false);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('--ci 非法值:用法错误并列出可取值', async () => {
|
|
492
|
+
const root = await makeProject();
|
|
493
|
+
roots.push(root);
|
|
494
|
+
// CLI 边界:commander 把用户敲的字符串原样递进来,合法域由命令层校验
|
|
495
|
+
const dirty = { ci: 'bitbucket' as unknown as InitOptions['ci'] };
|
|
496
|
+
|
|
497
|
+
const result = await init({ ...CI_OPTIONS, ...dirty, projectPath: root }, captureIO());
|
|
498
|
+
|
|
499
|
+
expect(result).toEqual({
|
|
500
|
+
kind: 'usage-error',
|
|
501
|
+
reason: expect.stringContaining('github | gitlab | none'),
|
|
502
|
+
});
|
|
503
|
+
expect(await exists(path.join(root, '.gitlab-ci.yml'))).toBe(false);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('config.yml 的 ci 段读不动(YAML 解析失败)时回落 github,不炸 init', async () => {
|
|
507
|
+
const root = await makeProject();
|
|
508
|
+
roots.push(root);
|
|
509
|
+
await fs.mkdir(path.join(root, '.harness'), { recursive: true });
|
|
510
|
+
await fs.writeFile(path.join(root, '.harness', 'config.yml'), 'preset: standard\nci:\n platform: [\n');
|
|
511
|
+
|
|
512
|
+
const io = captureIO();
|
|
513
|
+
expect(await init({ ...CI_OPTIONS, projectPath: root }, io)).toEqual({ kind: 'ok' });
|
|
514
|
+
|
|
515
|
+
expect(io.outText()).not.toContain('CI 平台');
|
|
516
|
+
expect(await exists(path.join(root, '.github/workflows/harness-check.yml'))).toBe(true);
|
|
517
|
+
expect(await exists(path.join(root, '.gitlab-ci.yml'))).toBe(false);
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// ── --ci × -g 组合边界(harness#156):none = 零 CI 文件;-g 值域校验在命令入口 ──
|
|
522
|
+
|
|
523
|
+
/** 项目下全部文件的相对路径(排序),逐字冻结「落盘文件集合」这个投影 */
|
|
524
|
+
async function listProjectFiles(root: string): Promise<string[]> {
|
|
525
|
+
const out: string[] = [];
|
|
526
|
+
async function walk(dir: string): Promise<void> {
|
|
527
|
+
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
|
|
528
|
+
const full = path.join(dir, entry.name);
|
|
529
|
+
if (entry.isDirectory()) await walk(full);
|
|
530
|
+
else out.push(path.relative(root, full).split(path.sep).join('/'));
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
await walk(root);
|
|
534
|
+
return out.sort();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
describe('init --ci × -g 组合边界(harness#156,真落盘)', () => {
|
|
538
|
+
const roots: string[] = [];
|
|
539
|
+
|
|
540
|
+
afterAll(async () => {
|
|
541
|
+
await Promise.all(roots.map(root => fs.rm(root, { recursive: true, force: true })));
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('--ci none -g standard:零 CI 文件、治理文档面照常;落盘文件集合与 config.yml 两投影逐字冻结', async () => {
|
|
545
|
+
const root = await makeProject();
|
|
546
|
+
roots.push(root);
|
|
547
|
+
const io = captureIO();
|
|
548
|
+
|
|
549
|
+
expect(await init({ ...INIT_OPTIONS, ci: 'none', projectPath: root }, io)).toEqual({ kind: 'ok' });
|
|
550
|
+
|
|
551
|
+
// 投影一:落盘文件集合逐字冻结。反证可跑——把 setupGovernanceWorkflow 的豁免写回
|
|
552
|
+
// `platform === 'gitlab'`(不豁免 none),集合里会多出
|
|
553
|
+
// `.github/workflows/harness-governance.yml`,本断言即红(harness#156 F1 的形态)
|
|
554
|
+
expect(await listProjectFiles(root)).toEqual([
|
|
555
|
+
'.git/hooks/pre-commit',
|
|
556
|
+
'.git/hooks/pre-push',
|
|
557
|
+
'.harness/checkpoints.yml',
|
|
558
|
+
'.harness/config.yml',
|
|
559
|
+
'.harness/custom-constraints.yml',
|
|
560
|
+
'.harness/resolutions.json',
|
|
561
|
+
'AGENTS.md',
|
|
562
|
+
'CHANGELOG.md',
|
|
563
|
+
'src/CONTEXT.md',
|
|
564
|
+
'src/index.ts',
|
|
565
|
+
]);
|
|
566
|
+
|
|
567
|
+
// 投影二:config.yml 逐字冻结(版本号随发布漂移,归一化)——ci.platform: none
|
|
568
|
+
// 与磁盘状态一致:配置声称不接任何 CI 平台,磁盘上也没有任何 CI 文件
|
|
569
|
+
const config = await fs.readFile(path.join(root, '.harness', 'config.yml'), 'utf-8');
|
|
570
|
+
expect(config.replace(/version: [\d.]+/, 'version: *')).toBe(`preset: standard
|
|
571
|
+
governance:
|
|
572
|
+
level: standard
|
|
573
|
+
docs:
|
|
574
|
+
sync_command: harness sync-docs
|
|
575
|
+
check_on_ci: true
|
|
576
|
+
files:
|
|
577
|
+
- CAPABILITIES.md
|
|
578
|
+
- README.md
|
|
579
|
+
context_files:
|
|
580
|
+
enabled: true
|
|
581
|
+
required_dirs: []
|
|
582
|
+
changelog:
|
|
583
|
+
format: keep-a-changelog
|
|
584
|
+
testing:
|
|
585
|
+
test_first: true
|
|
586
|
+
coverage_threshold: 85
|
|
587
|
+
incremental_coverage: false
|
|
588
|
+
ci:
|
|
589
|
+
platform: none
|
|
590
|
+
harness:
|
|
591
|
+
version: *
|
|
592
|
+
`);
|
|
593
|
+
|
|
594
|
+
// 治理文档面(CHANGELOG / 约束段 / CONTEXT.md)在 none 下照常生成,不受 CI 豁免影响
|
|
595
|
+
expect(await fs.readFile(path.join(root, 'CHANGELOG.md'), 'utf-8')).toContain('Keep a Changelog');
|
|
596
|
+
expect(await fs.readFile(path.join(root, 'AGENTS.md'), 'utf-8')).toContain('PRESERVE:governance');
|
|
597
|
+
expect(await fs.readFile(path.join(root, 'src/CONTEXT.md'), 'utf-8')).toBe(CONTEXT_MD_BYTES);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it('-g 非法值在 github / gitlab / none 三平台后果一致:usage-error、零落盘(钉「一致」本身)', async () => {
|
|
601
|
+
// CLI 边界:commander 把用户敲的字符串原样递进来,合法域由命令入口校验
|
|
602
|
+
const dirty = { governance: 'bogus' as unknown as InitOptions['governance'] };
|
|
603
|
+
const runs: Array<{ platform: string; result: unknown; files: string[]; err: string }> = [];
|
|
604
|
+
|
|
605
|
+
for (const platform of ['github', 'gitlab', 'none'] as const) {
|
|
606
|
+
const root = await makeProject();
|
|
607
|
+
roots.push(root);
|
|
608
|
+
const io = captureIO();
|
|
609
|
+
const result = await init({ ...CI_OPTIONS, ...dirty, ci: platform, projectPath: root }, io);
|
|
610
|
+
runs.push({ platform, result, files: await listProjectFiles(root), err: io.errText() });
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// 钉的是「三平台一致」这件事本身(harness#156 F2:同一非法输入,gitlab 曾落三个任务、
|
|
614
|
+
// github 曾静默不建治理站),不是各自冻一份基线
|
|
615
|
+
expect(runs[1].result).toEqual(runs[0].result);
|
|
616
|
+
expect(runs[2].result).toEqual(runs[0].result);
|
|
617
|
+
expect(runs[1].files).toEqual(runs[0].files);
|
|
618
|
+
expect(runs[2].files).toEqual(runs[0].files);
|
|
619
|
+
for (const run of runs) {
|
|
620
|
+
expect(run.result).toEqual({
|
|
621
|
+
kind: 'usage-error',
|
|
622
|
+
reason: expect.stringContaining('minimal | standard | strict'),
|
|
623
|
+
});
|
|
624
|
+
expect(run.err).toContain('用法错误');
|
|
625
|
+
// 零落盘:只剩夹具文件(src/index.ts;.git 是空目录不产生文件条目)
|
|
626
|
+
expect(run.files).toEqual(['src/index.ts']);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it.each(['constructor', 'toString'])(
|
|
631
|
+
'-g %s 不穿透原型链:usage-error、零落盘(harness#164:`in` 走原型链可拿到 Object 构造函数的反证)',
|
|
632
|
+
async dirty => {
|
|
633
|
+
const root = await makeProject();
|
|
634
|
+
roots.push(root);
|
|
635
|
+
const io = captureIO();
|
|
636
|
+
|
|
637
|
+
const result = await init(
|
|
638
|
+
{ ...CI_OPTIONS, governance: dirty as unknown as InitOptions['governance'], projectPath: root },
|
|
639
|
+
io,
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
expect(result).toEqual({
|
|
643
|
+
kind: 'usage-error',
|
|
644
|
+
reason: expect.stringContaining('minimal | standard | strict'),
|
|
645
|
+
});
|
|
646
|
+
expect(io.errText()).toContain('用法错误');
|
|
647
|
+
expect(await listProjectFiles(root)).toEqual(['src/index.ts']);
|
|
648
|
+
},
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
it.each(['minimal', 'standard', 'strict'] as const)(
|
|
652
|
+
'-g 合法值 %s 行为不变:ok 且治理级别写进 config.yml',
|
|
653
|
+
async level => {
|
|
654
|
+
const root = await makeProject();
|
|
655
|
+
roots.push(root);
|
|
656
|
+
|
|
657
|
+
expect(await init({ ...CI_OPTIONS, governance: level, projectPath: root }, captureIO()))
|
|
658
|
+
.toEqual({ kind: 'ok' });
|
|
659
|
+
|
|
660
|
+
const config = await fs.readFile(path.join(root, '.harness', 'config.yml'), 'utf-8');
|
|
661
|
+
expect(config).toContain(`level: ${level}`);
|
|
662
|
+
},
|
|
663
|
+
);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// ── --print-snippets 的 CI 片段视图(harness#153):打印的 job 正文 == 落盘正本的一段 ──
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* harness-check.yml 的字节级冻结基线(正本:`scaffold-templates.HARNESS_CHECK_WORKFLOW`)。
|
|
670
|
+
* 与 `GITLAB_PLAIN` 同族:改模板必须同步改这份基线,而「同步」这个动作正是拦住门禁被削弱的地方。
|
|
671
|
+
*/
|
|
672
|
+
const HARNESS_CHECK_BYTES = `name: Harness Check
|
|
673
|
+
|
|
674
|
+
on:
|
|
675
|
+
push:
|
|
676
|
+
branches: [main, master]
|
|
677
|
+
pull_request:
|
|
678
|
+
branches: [main, master]
|
|
679
|
+
|
|
680
|
+
jobs:
|
|
681
|
+
harness-check:
|
|
682
|
+
runs-on: ubuntu-latest
|
|
683
|
+
|
|
684
|
+
steps:
|
|
685
|
+
- uses: actions/checkout@v4
|
|
686
|
+
|
|
687
|
+
- name: Setup Node.js
|
|
688
|
+
uses: actions/setup-node@v4
|
|
689
|
+
with:
|
|
690
|
+
node-version: '20'
|
|
691
|
+
|
|
692
|
+
- name: Install dependencies
|
|
693
|
+
run: npm ci
|
|
694
|
+
|
|
695
|
+
- name: Run harness check
|
|
696
|
+
run: npx @dommaker/harness check
|
|
697
|
+
|
|
698
|
+
- name: Run harness validate
|
|
699
|
+
run: npx @dommaker/harness validate
|
|
700
|
+
|
|
701
|
+
- name: Run harness passes-gate
|
|
702
|
+
run: npx @dommaker/harness passes-gate
|
|
703
|
+
`;
|
|
704
|
+
|
|
705
|
+
/** 打印面里那份 job 正文(一次 log 调用 = 一条记录;片段自带的首尾换行按基线口径归一) */
|
|
706
|
+
function printedJob(io: CapturingIO): string {
|
|
707
|
+
const record = io.outRecords().find(chunk => chunk.includes('harness-check:'));
|
|
708
|
+
if (!record) throw new Error('--print-snippets 没有打印 harness-check 的 job 正文');
|
|
709
|
+
return record.replace(/^\n+/, '').replace(/\n$/, '');
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** job 正文(含两空格缩进)的 run 命令清单:按 YAML 解析枚举 steps,不靠子串碰运气 */
|
|
713
|
+
function runCommands(jobBody: string): string[] {
|
|
714
|
+
const doc = yaml.load(`jobs:\n${jobBody}`) as {
|
|
715
|
+
jobs: Record<string, { steps?: Array<{ run?: string }> }>;
|
|
716
|
+
};
|
|
717
|
+
return (doc.jobs['harness-check'].steps ?? [])
|
|
718
|
+
.map(step => step.run)
|
|
719
|
+
.filter((run): run is string => run !== undefined);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const GH_GUIDANCE_LINE = '添加到 .github/workflows/*.yml 的 jobs 下(以下正文取自 harness-check.yml 的 job 段)';
|
|
723
|
+
|
|
724
|
+
describe('init --print-snippets 的 GitHub Actions 片段视图(harness#153)', () => {
|
|
725
|
+
const roots: string[] = [];
|
|
726
|
+
|
|
727
|
+
afterAll(async () => {
|
|
728
|
+
await Promise.all(roots.map(root => fs.rm(root, { recursive: true, force: true })));
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
async function snippetsOfCiView(): Promise<CapturingIO> {
|
|
732
|
+
const root = await makeProject();
|
|
733
|
+
roots.push(root);
|
|
734
|
+
const io = captureIO();
|
|
735
|
+
await init({ ...CI_OPTIONS, printSnippets: true, projectPath: root }, io);
|
|
736
|
+
return io;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
it('同源闸:打印的 job 正文逐字等于落盘正本的 jobs: 段之后(抄回去的就是 init 写出的那一段)', async () => {
|
|
740
|
+
const printed = await snippetsOfCiView();
|
|
741
|
+
const job = printedJob(printed);
|
|
742
|
+
|
|
743
|
+
expect(job).toBe(HARNESS_CHECK_BYTES.slice(HARNESS_CHECK_BYTES.indexOf(' harness-check:')));
|
|
744
|
+
|
|
745
|
+
const root = await makeProject();
|
|
746
|
+
roots.push(root);
|
|
747
|
+
await init({ ...CI_OPTIONS, projectPath: root }, captureIO());
|
|
748
|
+
const written = await fs.readFile(path.join(root, '.github/workflows/harness-check.yml'), 'utf-8');
|
|
749
|
+
expect(written.includes(job)).toBe(true);
|
|
750
|
+
expect(runCommands(written.slice(written.indexOf(' harness-check:')))).toEqual(runCommands(job));
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
it('steps 集合闸:打印的 job 覆盖 check / validate / passes-gate 三道门禁,一道不缺', async () => {
|
|
754
|
+
const printed = await snippetsOfCiView();
|
|
755
|
+
|
|
756
|
+
expect(runCommands(printedJob(printed))).toEqual(
|
|
757
|
+
expect.arrayContaining([
|
|
758
|
+
'npx @dommaker/harness check',
|
|
759
|
+
'npx @dommaker/harness validate',
|
|
760
|
+
'npx @dommaker/harness passes-gate',
|
|
761
|
+
]),
|
|
762
|
+
);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it('引导语逐字冻结:说清打印的是完整 workflow 的 job 段', async () => {
|
|
766
|
+
const lines = (await snippetsOfCiView()).outLines();
|
|
767
|
+
|
|
768
|
+
expect(lines[lines.indexOf('GitHub Actions:') + 1]).toBe(GH_GUIDANCE_LINE);
|
|
769
|
+
expect(lines).not.toContain('添加到 .github/workflows/*.yml 的 jobs 中');
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// ── --print-snippets 的 GitLab CI 片段视图(harness#157):打印的任务正文 == 落盘正本的一段 ──
|
|
774
|
+
|
|
775
|
+
/** 打印面里 GitLab 那份任务正文(一次 log 调用 = 一条记录;归一化口径同 printedJob) */
|
|
776
|
+
function printedGitLabJobs(io: CapturingIO): string {
|
|
777
|
+
const record = io.outRecords().find(chunk => chunk.includes('harness-check:'));
|
|
778
|
+
if (!record) throw new Error('--print-snippets 没有打印 harness-check 的任务正文');
|
|
779
|
+
return record.replace(/^\n+/, '').replace(/\n$/, '');
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** .gitlab-ci.yml 文本里 harness 拥有的任务名(顶层键中 harness-* 的那些),排序后比对 */
|
|
783
|
+
function harnessJobNames(gitlabCiText: string): string[] {
|
|
784
|
+
const doc = yaml.load(gitlabCiText) as Record<string, unknown>;
|
|
785
|
+
return Object.keys(doc).filter(key => key.startsWith('harness-')).sort();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** GitLab 任务正文里某任务的 script 命令清单:按 YAML 解析枚举,不靠子串碰运气 */
|
|
789
|
+
function gitLabScriptCommands(jobsBody: string, job: string): string[] {
|
|
790
|
+
const doc = yaml.load(jobsBody) as Record<string, { script?: string[] }>;
|
|
791
|
+
return doc[job]?.script ?? [];
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
describe('init --print-snippets 的 GitLab CI 片段视图(harness#157)', () => {
|
|
795
|
+
const roots: string[] = [];
|
|
796
|
+
|
|
797
|
+
afterAll(async () => {
|
|
798
|
+
await Promise.all(roots.map(root => fs.rm(root, { recursive: true, force: true })));
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
async function gitlabSnippets(options: Partial<InitOptions> = {}) {
|
|
802
|
+
const root = await makeProject();
|
|
803
|
+
roots.push(root);
|
|
804
|
+
const io = captureIO();
|
|
805
|
+
const result = await init(
|
|
806
|
+
{ ...CI_OPTIONS, ci: 'gitlab', printSnippets: true, ...options, projectPath: root },
|
|
807
|
+
io,
|
|
808
|
+
);
|
|
809
|
+
return { io, result };
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
it('同源闸(-g standard):打印的任务正文逐字等于落盘正本的任务段,任务集合与真落盘文件一致', async () => {
|
|
813
|
+
const { io } = await gitlabSnippets({ governance: 'standard' });
|
|
814
|
+
const printed = printedGitLabJobs(io);
|
|
815
|
+
|
|
816
|
+
// 逐字等于字节级冻结基线的任务段(改模板必须同步改 GITLAB_GOVERNED——同步动作正是闸)
|
|
817
|
+
expect(printed).toBe(
|
|
818
|
+
GITLAB_GOVERNED.slice(GITLAB_GOVERNED.indexOf('harness-check:')),
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
// 与真落盘对撞:任务集合按 YAML 解析比对(非子串)——给落盘正本删一个任务,
|
|
822
|
+
// 集合即不等,本闸必红(harness#157 验收 2 的反证)
|
|
823
|
+
const root = await makeProject();
|
|
824
|
+
roots.push(root);
|
|
825
|
+
await init({ ...INIT_OPTIONS, ci: 'gitlab', projectPath: root }, captureIO());
|
|
826
|
+
const written = await fs.readFile(path.join(root, '.gitlab-ci.yml'), 'utf-8');
|
|
827
|
+
expect(harnessJobNames(written)).toEqual(harnessJobNames(printed));
|
|
828
|
+
expect(written).toContain(printed);
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
it('script 集合闸:打印的 harness-check 覆盖 check / validate / passes-gate 三道门禁,一道不缺', async () => {
|
|
832
|
+
const { io } = await gitlabSnippets({ governance: 'standard' });
|
|
833
|
+
|
|
834
|
+
expect(gitLabScriptCommands(printedGitLabJobs(io), 'harness-check')).toEqual(
|
|
835
|
+
expect.arrayContaining([
|
|
836
|
+
'npx @dommaker/harness check',
|
|
837
|
+
'npx @dommaker/harness validate',
|
|
838
|
+
'npx @dommaker/harness passes-gate',
|
|
839
|
+
]),
|
|
840
|
+
);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('不传 -g:只打 harness-check 一个任务,与无治理档的落盘正本一致', async () => {
|
|
844
|
+
const { io } = await gitlabSnippets();
|
|
845
|
+
const printed = printedGitLabJobs(io);
|
|
846
|
+
|
|
847
|
+
expect(printed).toBe(
|
|
848
|
+
GITLAB_PLAIN.slice(GITLAB_PLAIN.indexOf('harness-check:')),
|
|
849
|
+
);
|
|
850
|
+
expect(harnessJobNames(printed)).toEqual(['harness-check']);
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it('-g 非法值:usage-error(harness#156 裁决 F2 同判),不打印任何 CI 片段', async () => {
|
|
854
|
+
// CLI 边界:commander 把用户敲的字符串原样递进来,合法域由命令入口校验
|
|
855
|
+
const dirty = { governance: 'bogus' as unknown as InitOptions['governance'] };
|
|
856
|
+
|
|
857
|
+
const { io, result } = await gitlabSnippets(dirty);
|
|
858
|
+
|
|
859
|
+
expect(result).toEqual({
|
|
860
|
+
kind: 'usage-error',
|
|
861
|
+
reason: expect.stringContaining('minimal | standard | strict'),
|
|
862
|
+
});
|
|
863
|
+
expect(io.outText()).not.toContain('GitLab CI:');
|
|
864
|
+
expect(io.outText()).not.toContain('harness-check:');
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
|