@haposoft/cafekit 0.16.0-rc.7 → 0.16.0
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/README.md +116 -27
- package/bin/__tests__/codex-hooks-ownership.test.js +239 -0
- package/bin/__tests__/codex-hooks.test.js +295 -0
- package/bin/__tests__/codex-native.test.js +2289 -34
- package/bin/__tests__/develop-contract.test.js +400 -23
- package/bin/__tests__/installer-safety.test.js +3 -3
- package/bin/__tests__/optional-skill-inventory.test.js +249 -0
- package/bin/__tests__/output-styles.test.js +214 -0
- package/bin/__tests__/package-inventory.test.js +2131 -16
- package/bin/__tests__/runtime-schema.test.js +111 -0
- package/bin/__tests__/secret-output-guardrail.test.js +122 -0
- package/bin/__tests__/skill-routing-source.test.js +172 -0
- package/bin/__tests__/specs-v2-execution-closeout.test.js +1 -1
- package/bin/install.js +10 -0
- package/bin/lib/codex-install.js +170 -5
- package/bin/lib/context.js +14 -0
- package/bin/lib/i18n.js +12 -6
- package/bin/lib/skill-deps.js +5 -5
- package/bin/phases/claude-runtime.js +23 -0
- package/bin/phases/claude-settings.js +8 -0
- package/bin/phases/codex-hooks.js +156 -0
- package/bin/phases/codex-runtime.js +29 -38
- package/bin/phases/copy-payload.js +8 -2
- package/bin/phases/select-skill-bundles.js +96 -0
- package/bin/phases/skill-inventory.js +71 -0
- package/bin/phases/skills-setup.js +9 -2
- package/bin/phases/write-metadata.js +8 -5
- package/package.json +8 -2
- package/src/claude/CLAUDE.md +3 -3
- package/src/claude/agents/brainstormer.md +52 -69
- package/src/claude/agents/code-auditor.md +32 -14
- package/src/claude/agents/debugger.md +35 -20
- package/src/claude/agents/deployer.md +12 -5
- package/src/claude/agents/docs-keeper.md +11 -1
- package/src/claude/agents/implementer.md +25 -13
- package/src/claude/agents/inspector.md +6 -3
- package/src/claude/agents/project-manager.md +1 -1
- package/src/claude/agents/researcher.md +26 -55
- package/src/claude/agents/spec-maker.md +24 -13
- package/src/claude/agents/strategist.md +52 -0
- package/src/claude/agents/test-runner.md +117 -132
- package/src/claude/agents/ui-ux-designer.md +1 -1
- package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -0
- package/src/claude/hooks/__tests__/precompact.test.js +158 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +87 -4
- package/src/claude/hooks/__tests__/state.test.js +254 -3
- package/src/claude/hooks/__tests__/statusline.test.js +196 -0
- package/src/claude/hooks/agent.cjs +1 -1
- package/src/claude/hooks/docs-sync.cjs +12 -10
- package/src/claude/hooks/inspect-block.cjs +1 -1
- package/src/claude/hooks/lib/config.cjs +3 -0
- package/src/claude/hooks/lib/hook-state-dir.cjs +39 -0
- package/src/claude/hooks/lib/secret-keywords.cjs +42 -0
- package/src/claude/hooks/precompact.cjs +105 -0
- package/src/claude/hooks/rules.cjs +1 -1
- package/src/claude/hooks/secret-output-guardrail.cjs +43 -0
- package/src/claude/hooks/session.cjs +27 -1
- package/src/claude/hooks/spec-gate.cjs +3 -3
- package/src/claude/hooks/spec-state.cjs +32 -5
- package/src/claude/hooks/state.cjs +1 -1
- package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
- package/src/claude/hooks/usage.cjs +1 -1
- package/src/claude/migration-manifest.json +29 -15
- package/src/claude/output-styles/coding-level-0-eli5.md +103 -0
- package/src/claude/output-styles/coding-level-1-junior.md +124 -0
- package/src/claude/output-styles/coding-level-2-mid.md +146 -0
- package/src/claude/output-styles/coding-level-3-senior.md +148 -0
- package/src/claude/output-styles/coding-level-4-lead.md +159 -0
- package/src/claude/output-styles/coding-level-5-god.md +91 -0
- package/src/claude/references/debugger/core-philosophy.md +5 -3
- package/src/claude/references/debugger/frontend-verification.md +2 -2
- package/src/claude/references/debugger/log-ci-analysis.md +13 -1
- package/src/claude/references/debugger/parallel-agent-hydration.md +19 -34
- package/src/claude/references/debugger/performance-diagnostics.md +2 -2
- package/src/claude/references/debugger/root-cause-tracing.md +37 -6
- package/src/claude/references/debugger/side-effect-gate.md +3 -1
- package/src/claude/references/debugger/verification-protocol.md +13 -12
- package/src/claude/rules/orchestrator.md +7 -1
- package/src/claude/rules/process-management.md +39 -0
- package/src/claude/rules/review-audit-self-decision.md +48 -0
- package/src/claude/rules/skill-domain-routing.md +26 -24
- package/src/claude/rules/skill-workflow-routing.md +22 -14
- package/src/claude/rules/workflow.md +3 -3
- package/src/claude/runtime.json +1 -0
- package/src/claude/runtime.schema.json +205 -0
- package/src/claude/scripts/generate-skill-catalog.cjs +156 -122
- package/src/claude/scripts/provenance.cjs +18 -5
- package/src/claude/scripts/spec-receipt.cjs +76 -21
- package/src/claude/scripts/spec-resolver.cjs +144 -4
- package/src/claude/scripts/workflow-policy.cjs +1 -1
- package/src/claude/settings/settings.json +16 -0
- package/src/claude/skills/agent-browser/SKILL.md +1 -1
- package/src/claude/skills/ai-multimodal/SKILL.md +1 -1
- package/src/claude/skills/brainstorm/SKILL.md +193 -172
- package/src/claude/skills/brainstorm/references/question-framework.md +105 -167
- package/src/claude/skills/chrome-devtools/SKILL.md +1 -1
- package/src/claude/skills/code-review/SKILL.md +43 -25
- package/src/claude/skills/code-review/references/spec-compliance-review.md +3 -4
- package/src/claude/skills/code-review/references/verification-gate.md +1 -1
- package/src/claude/skills/debug/SKILL.md +48 -19
- package/src/claude/skills/delegate/SKILL.md +1 -1
- package/src/claude/skills/develop/SKILL.md +92 -65
- package/src/claude/skills/develop/references/parallel-waves.md +36 -63
- package/src/claude/skills/develop/references/quality-gate.md +31 -4
- package/src/claude/skills/develop/references/subagent-patterns.md +21 -7
- package/src/claude/skills/docs/SKILL.md +42 -23
- package/src/claude/skills/docs/references/init-workflow.md +4 -4
- package/src/claude/skills/docs/references/reconstruct-workflow.md +6 -6
- package/src/claude/skills/docs/references/standard-docs-workflow.md +6 -2
- package/src/claude/skills/docs/references/summarize-workflow.md +2 -2
- package/src/claude/skills/docs/references/update-workflow.md +9 -7
- package/src/claude/skills/docs/templates/requirements-as-is.md +1 -1
- package/src/claude/skills/docx/SKILL.md +1 -1
- package/src/claude/skills/git/SKILL.md +1 -1
- package/src/claude/skills/git/references/commit-protocols.md +1 -1
- package/src/claude/skills/git/references/finish-branch.md +1 -1
- package/src/claude/skills/hotfix/SKILL.md +159 -170
- package/src/claude/skills/hotfix/references/diagnosis-protocol.md +14 -9
- package/src/claude/skills/hotfix/references/parallel-patterns.md +27 -13
- package/src/claude/skills/hotfix/references/prevention-gate.md +5 -0
- package/src/claude/skills/hotfix/references/review-cycle.md +21 -15
- package/src/claude/skills/hotfix/references/workflow-specialized.md +53 -21
- package/src/claude/skills/inspect/SKILL.md +55 -46
- package/src/claude/skills/inspect/references/internal-inspection.md +21 -5
- package/src/claude/skills/loop/SKILL.md +114 -0
- package/src/claude/skills/loop/references/bounded-loop-protocol.md +104 -0
- package/src/claude/skills/loop/references/metric-and-guard-contract.md +76 -0
- package/src/claude/skills/pdf/SKILL.md +1 -1
- package/src/claude/skills/pptx/SKILL.md +1 -1
- package/src/claude/skills/question/SKILL.md +16 -13
- package/src/claude/skills/research/SKILL.md +70 -43
- package/src/claude/skills/route/SKILL.md +94 -0
- package/src/claude/skills/route/references/agent-timing.md +46 -0
- package/src/claude/skills/route/references/chaining-patterns.md +53 -0
- package/src/claude/skills/route/references/task-taxonomy.md +48 -0
- package/src/claude/skills/specs/SKILL.md +48 -57
- package/src/claude/skills/specs/references/review.md +26 -8
- package/src/claude/skills/specs/references/templates.md +76 -31
- package/src/claude/skills/specs/templates/requirements-init.md +1 -1
- package/src/claude/skills/specs/templates/research.md +2 -2
- package/src/claude/skills/sync/SKILL.md +6 -6
- package/src/claude/skills/sync/references/sync-protocols.md +1 -1
- package/src/claude/skills/test/SKILL.md +120 -120
- package/src/claude/skills/test/references/execution-strategy.md +208 -361
- package/src/claude/skills/test/references/failure-triage.md +55 -121
- package/src/claude/skills/test/references/test-memory.md +20 -34
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
- package/src/claude/skills/web-testing/SKILL.md +1 -1
- package/src/claude/skills/xlsx/SKILL.md +1 -1
- package/src/claude/status.cjs +135 -33
- package/src/codex/AGENTS.md +14 -4
- package/src/codex/hooks/docs-sync.cjs +8 -3
- package/src/codex/hooks/lib/hook-context.cjs +18 -4
- package/src/codex/hooks/rules.cjs +29 -1
- package/src/codex/hooks/secret-output-guardrail.cjs +30 -0
- package/src/codex/hooks/spec-gate.cjs +3 -8
- package/src/codex/hooks/spec-state.cjs +34 -5
- package/src/codex/hooks.json +5 -0
- package/src/codex/runtime.json +2 -0
- package/src/common/.env.example +1 -1
- package/src/claude/skills/backend-development/SKILL.md +0 -101
- package/src/claude/skills/backend-development/references/backend-api-design.md +0 -495
- package/src/claude/skills/backend-development/references/backend-architecture.md +0 -454
- package/src/claude/skills/backend-development/references/backend-authentication.md +0 -338
- package/src/claude/skills/backend-development/references/backend-code-quality.md +0 -659
- package/src/claude/skills/backend-development/references/backend-debugging.md +0 -904
- package/src/claude/skills/backend-development/references/backend-devops.md +0 -491
- package/src/claude/skills/backend-development/references/backend-mindset.md +0 -387
- package/src/claude/skills/backend-development/references/backend-performance.md +0 -397
- package/src/claude/skills/backend-development/references/backend-security.md +0 -342
- package/src/claude/skills/backend-development/references/backend-technologies.md +0 -256
- package/src/claude/skills/backend-development/references/backend-testing.md +0 -429
- package/src/claude/skills/devops/.env.example +0 -76
- package/src/claude/skills/devops/SKILL.md +0 -102
- package/src/claude/skills/devops/references/browser-rendering.md +0 -305
- package/src/claude/skills/devops/references/cloudflare-d1-kv.md +0 -123
- package/src/claude/skills/devops/references/cloudflare-platform.md +0 -271
- package/src/claude/skills/devops/references/cloudflare-r2-storage.md +0 -280
- package/src/claude/skills/devops/references/cloudflare-workers-advanced.md +0 -312
- package/src/claude/skills/devops/references/cloudflare-workers-apis.md +0 -309
- package/src/claude/skills/devops/references/cloudflare-workers-basics.md +0 -418
- package/src/claude/skills/devops/references/docker-basics.md +0 -297
- package/src/claude/skills/devops/references/docker-compose.md +0 -292
- package/src/claude/skills/devops/references/gcloud-platform.md +0 -297
- package/src/claude/skills/devops/references/gcloud-services.md +0 -304
- package/src/claude/skills/devops/references/kubernetes-basics.md +0 -95
- package/src/claude/skills/devops/references/kubernetes-helm-advanced.md +0 -70
- package/src/claude/skills/devops/references/kubernetes-helm.md +0 -81
- package/src/claude/skills/devops/references/kubernetes-kubectl.md +0 -74
- package/src/claude/skills/devops/references/kubernetes-security-advanced.md +0 -94
- package/src/claude/skills/devops/references/kubernetes-security.md +0 -89
- package/src/claude/skills/devops/references/kubernetes-troubleshooting-advanced.md +0 -74
- package/src/claude/skills/devops/references/kubernetes-troubleshooting.md +0 -49
- package/src/claude/skills/devops/references/kubernetes-workflows-advanced.md +0 -75
- package/src/claude/skills/devops/references/kubernetes-workflows.md +0 -76
- package/src/claude/skills/devops/scripts/cloudflare_deploy.py +0 -269
- package/src/claude/skills/devops/scripts/docker_optimize.py +0 -332
- package/src/claude/skills/devops/scripts/requirements.txt +0 -20
- package/src/claude/skills/devops/scripts/tests/requirements.txt +0 -3
- package/src/claude/skills/devops/scripts/tests/test_cloudflare_deploy.py +0 -285
- package/src/claude/skills/devops/scripts/tests/test_docker_optimize.py +0 -436
- package/src/claude/skills/frontend-design/SKILL.md +0 -135
- package/src/claude/skills/frontend-design/references/ai-multimodal-overview.md +0 -165
- package/src/claude/skills/frontend-design/references/analysis-best-practices.md +0 -80
- package/src/claude/skills/frontend-design/references/analysis-prompts.md +0 -141
- package/src/claude/skills/frontend-design/references/analysis-techniques.md +0 -118
- package/src/claude/skills/frontend-design/references/animejs.md +0 -396
- package/src/claude/skills/frontend-design/references/anti-slop-rules.md +0 -103
- package/src/claude/skills/frontend-design/references/asset-generation.md +0 -337
- package/src/claude/skills/frontend-design/references/bento-motion-engine.md +0 -142
- package/src/claude/skills/frontend-design/references/design-extraction-overview.md +0 -71
- package/src/claude/skills/frontend-design/references/extraction-best-practices.md +0 -141
- package/src/claude/skills/frontend-design/references/extraction-output-templates.md +0 -162
- package/src/claude/skills/frontend-design/references/extraction-prompts.md +0 -127
- package/src/claude/skills/frontend-design/references/magicui-components.md +0 -129
- package/src/claude/skills/frontend-design/references/performance-guardrails.md +0 -169
- package/src/claude/skills/frontend-design/references/premium-design-patterns.md +0 -93
- package/src/claude/skills/frontend-design/references/redesign-audit-checklist.md +0 -114
- package/src/claude/skills/frontend-design/references/technical-accessibility.md +0 -119
- package/src/claude/skills/frontend-design/references/technical-best-practices.md +0 -97
- package/src/claude/skills/frontend-design/references/technical-optimization.md +0 -44
- package/src/claude/skills/frontend-design/references/technical-overview.md +0 -90
- package/src/claude/skills/frontend-design/references/technical-workflows.md +0 -150
- package/src/claude/skills/frontend-design/references/visual-analysis-overview.md +0 -95
- package/src/claude/skills/frontend-design/references/workflow-3d.md +0 -102
- package/src/claude/skills/frontend-design/references/workflow-describe.md +0 -87
- package/src/claude/skills/frontend-design/references/workflow-immersive.md +0 -87
- package/src/claude/skills/frontend-design/references/workflow-quick.md +0 -57
- package/src/claude/skills/frontend-design/references/workflow-screenshot.md +0 -63
- package/src/claude/skills/frontend-design/references/workflow-video.md +0 -74
- package/src/claude/skills/frontend-development/SKILL.md +0 -406
- package/src/claude/skills/frontend-development/resources/common-patterns.md +0 -331
- package/src/claude/skills/frontend-development/resources/complete-examples.md +0 -872
- package/src/claude/skills/frontend-development/resources/component-patterns.md +0 -502
- package/src/claude/skills/frontend-development/resources/data-fetching.md +0 -767
- package/src/claude/skills/frontend-development/resources/file-organization.md +0 -502
- package/src/claude/skills/frontend-development/resources/loading-and-error-states.md +0 -501
- package/src/claude/skills/frontend-development/resources/performance.md +0 -406
- package/src/claude/skills/frontend-development/resources/routing-guide.md +0 -364
- package/src/claude/skills/frontend-development/resources/styling-guide.md +0 -428
- package/src/claude/skills/frontend-development/resources/typescript-standards.md +0 -418
- package/src/claude/skills/mobile-development/SKILL.md +0 -218
- package/src/claude/skills/mobile-development/references/mobile-android.md +0 -604
- package/src/claude/skills/mobile-development/references/mobile-best-practices.md +0 -544
- package/src/claude/skills/mobile-development/references/mobile-debugging.md +0 -1089
- package/src/claude/skills/mobile-development/references/mobile-frameworks.md +0 -465
- package/src/claude/skills/mobile-development/references/mobile-ios.md +0 -496
- package/src/claude/skills/mobile-development/references/mobile-mindset.md +0 -542
- package/src/claude/skills/react-best-practices/AGENTS.md +0 -2249
- package/src/claude/skills/react-best-practices/README.md +0 -123
- package/src/claude/skills/react-best-practices/SKILL.md +0 -128
- package/src/claude/skills/react-best-practices/metadata.json +0 -15
- package/src/claude/skills/react-best-practices/rules/_sections.md +0 -46
- package/src/claude/skills/react-best-practices/rules/_template.md +0 -28
- package/src/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +0 -55
- package/src/claude/skills/react-best-practices/rules/advanced-use-latest.md +0 -49
- package/src/claude/skills/react-best-practices/rules/async-api-routes.md +0 -38
- package/src/claude/skills/react-best-practices/rules/async-defer-await.md +0 -80
- package/src/claude/skills/react-best-practices/rules/async-dependencies.md +0 -36
- package/src/claude/skills/react-best-practices/rules/async-parallel.md +0 -28
- package/src/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +0 -99
- package/src/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +0 -59
- package/src/claude/skills/react-best-practices/rules/bundle-conditional.md +0 -31
- package/src/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +0 -49
- package/src/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +0 -35
- package/src/claude/skills/react-best-practices/rules/bundle-preload.md +0 -50
- package/src/claude/skills/react-best-practices/rules/client-event-listeners.md +0 -74
- package/src/claude/skills/react-best-practices/rules/client-swr-dedup.md +0 -56
- package/src/claude/skills/react-best-practices/rules/js-batch-dom-css.md +0 -82
- package/src/claude/skills/react-best-practices/rules/js-cache-function-results.md +0 -80
- package/src/claude/skills/react-best-practices/rules/js-cache-property-access.md +0 -28
- package/src/claude/skills/react-best-practices/rules/js-cache-storage.md +0 -70
- package/src/claude/skills/react-best-practices/rules/js-combine-iterations.md +0 -32
- package/src/claude/skills/react-best-practices/rules/js-early-exit.md +0 -50
- package/src/claude/skills/react-best-practices/rules/js-hoist-regexp.md +0 -45
- package/src/claude/skills/react-best-practices/rules/js-index-maps.md +0 -37
- package/src/claude/skills/react-best-practices/rules/js-length-check-first.md +0 -49
- package/src/claude/skills/react-best-practices/rules/js-min-max-loop.md +0 -82
- package/src/claude/skills/react-best-practices/rules/js-set-map-lookups.md +0 -24
- package/src/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +0 -57
- package/src/claude/skills/react-best-practices/rules/rendering-activity.md +0 -26
- package/src/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
- package/src/claude/skills/react-best-practices/rules/rendering-conditional-render.md +0 -40
- package/src/claude/skills/react-best-practices/rules/rendering-content-visibility.md +0 -38
- package/src/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +0 -46
- package/src/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
- package/src/claude/skills/react-best-practices/rules/rendering-svg-precision.md +0 -28
- package/src/claude/skills/react-best-practices/rules/rerender-defer-reads.md +0 -39
- package/src/claude/skills/react-best-practices/rules/rerender-dependencies.md +0 -45
- package/src/claude/skills/react-best-practices/rules/rerender-derived-state.md +0 -29
- package/src/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +0 -74
- package/src/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +0 -58
- package/src/claude/skills/react-best-practices/rules/rerender-memo.md +0 -44
- package/src/claude/skills/react-best-practices/rules/rerender-transitions.md +0 -40
- package/src/claude/skills/react-best-practices/rules/server-after-nonblocking.md +0 -73
- package/src/claude/skills/react-best-practices/rules/server-cache-lru.md +0 -41
- package/src/claude/skills/react-best-practices/rules/server-cache-react.md +0 -26
- package/src/claude/skills/react-best-practices/rules/server-parallel-fetching.md +0 -79
- package/src/claude/skills/react-best-practices/rules/server-serialization.md +0 -38
|
@@ -4,10 +4,109 @@ const assert = require('node:assert/strict');
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const os = require('node:os');
|
|
6
6
|
const path = require('node:path');
|
|
7
|
+
const { createHash } = require('node:crypto');
|
|
7
8
|
const { spawnSync } = require('node:child_process');
|
|
8
9
|
const { test } = require('node:test');
|
|
9
10
|
|
|
11
|
+
const {
|
|
12
|
+
convertCodexAgentContent,
|
|
13
|
+
normalizeCodexBody,
|
|
14
|
+
} = require('../lib/codex-install');
|
|
15
|
+
|
|
10
16
|
const PACKAGE_ROOT = path.resolve(__dirname, '../..');
|
|
17
|
+
const PACKAGE_VERSION = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8')).version;
|
|
18
|
+
const OLD_FIXTURE_VERSION = '0.14.1';
|
|
19
|
+
const NO_INVENTION_ANCHOR = 'Before implementation handoff, apply the **no-invention gate**:';
|
|
20
|
+
const IMPLEMENTATION_READINESS_BOUNDARY_ROWS = [
|
|
21
|
+
['Interaction/UI', 'entry journey; visible/loading/empty/error states; input/focus/keyboard; accessibility; responsive/native/device behavior'],
|
|
22
|
+
['API/CLI', 'entrypoint/route or command grammar; identity/auth; input/default/normalization; success output; error/status/exit; duplicate/retry/idempotency; compatibility'],
|
|
23
|
+
['Data/schema', 'authority/storage/transaction; version; exact keys/nesting/types; required/optional; enum/format/bounds/cardinality; unknown-field behavior; compatibility/migration'],
|
|
24
|
+
['Async/state', 'initial/terminal states; event + guard + effect + next + error; ordering/concurrency; duplicate/retry; writer/lock acquire/contention/release; cancellation; rollback/recovery'],
|
|
25
|
+
['Filesystem/security', 'authoritative root; trusted/untrusted segment grammar; lexical + canonical containment and symlink policy; flags/mode; temp/rename/fsync; lock/stale reclaim; crash cleanup'],
|
|
26
|
+
['Runtime/deploy', 'config/env/flags; registration/packaging; OS/arch; rollout/rollback; health/logging; operator recovery'],
|
|
27
|
+
['Time/retention', 'clock source; unit/precision/timezone; endpoints and inclusion/comparator; anomaly behavior; expiry/purge/recovery'],
|
|
28
|
+
['AI/model', 'provider/model/prompt/tool schema; nondeterminism/bounds; safety/privacy; fallback; cost/token limit; eval oracle'],
|
|
29
|
+
['Integration/proof', 'caller; export/registration; packaging/config; native path/consumer; proof level (`source`/`installed`/`live`); observable failure oracle'],
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function boundaryTableHasRequiredRows(table) {
|
|
33
|
+
const [header, ...rows] = table;
|
|
34
|
+
const labels = rows.map((row) => row[0]);
|
|
35
|
+
return JSON.stringify(header || []) === JSON.stringify(['Boundary', 'Required contract when material'])
|
|
36
|
+
&& new Set(labels).size === labels.length
|
|
37
|
+
&& IMPLEMENTATION_READINESS_BOUNDARY_ROWS.every((expected) =>
|
|
38
|
+
rows.some((row) => JSON.stringify(row) === JSON.stringify(expected)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const IMPLEMENTATION_READINESS_CLAUSES = {
|
|
42
|
+
noInvention: 'Before implementation handoff, apply the **no-invention gate**: if two implementations conform to the packet text yet can produce different externally observable output, state, error, security, or compatibility behavior, surface the missing choice as an explicit C1 or C2 question and block handoff.',
|
|
43
|
+
materialDefinition: 'A boundary is material when the task creates, changes, or depends on it and a different choice changes an external observation, security, durable data, compatibility, or proof reachability. Require only the matching material row; omit nonmaterial categories.',
|
|
44
|
+
exactBoundaryChoices: 'For every required row, name each listed choice exactly; labels such as “JSON”, “local path”, “locked”, or “timestamped” alone remain unresolved.',
|
|
45
|
+
proofPlanLines: [
|
|
46
|
+
'- Command: `<exact runnable command>`',
|
|
47
|
+
'- Named probe: <existing concrete probe/test/hook ID; never only a suite label>',
|
|
48
|
+
'- Reachability: <known command/caller/environment per required level; `UNKNOWN` only when the path cannot yet be established>',
|
|
49
|
+
'- Oracle: <externally observable success or failure>',
|
|
50
|
+
'- Counterexample: <material alternative behavior that must make this proof fail>',
|
|
51
|
+
'- Artifacts: <required artifact path plus digest algorithm/comparison rule, or explicitly ephemeral with cleanup rule>',
|
|
52
|
+
],
|
|
53
|
+
proofTrace: 'Trace `Command → Named probe → Reachability → Oracle`.',
|
|
54
|
+
namedProbeOwnership: 'Aggregate suites name the\nowning concrete probe.',
|
|
55
|
+
proofLevelSeparation: 'Levels stay separate and never promote one another.',
|
|
56
|
+
disposableTemplateControls: 'Run mutation or destructive\nnegative controls only on disposable copies under a verified temporary root,\nnever tracked worktree or canonical source bytes.',
|
|
57
|
+
proofLevelMapping: 'For every required level in each referenced CP row, map its named probe and\nreachability here; one command may own several explicitly named level probes.',
|
|
58
|
+
disposableReviewControls: 'Run mutation or destructive negative controls only on disposable copies below a verified temporary root, never tracked worktree or canonical source bytes.',
|
|
59
|
+
failureSemantics: '`Crash` means abrupt unhandled termination before the claimed catch point; a catchable failure returns/raises an error or exits nonzero. Never use them interchangeably.',
|
|
60
|
+
privacyIdentifiers: 'Any privacy/security claim names the exact identifier surface at risk, such as an env var, header, path, token class, or field name; generic “sensitive data” is insufficient.',
|
|
61
|
+
freshReplay: 'After applying an accepted C2 finding, a fresh-context closure pass records and freshly replays its original counterexample after the repair under this exact review-log header:',
|
|
62
|
+
distinctRepairProof: '`Repaired at` cites the repair edit; `Proved at` must cite distinct evidence from the fresh replay, never the repair-edit citation.',
|
|
63
|
+
closureTransition: 'An accepted finding transitions `accepted → repaired → PASS|FAIL|UNKNOWN`.',
|
|
64
|
+
unknownBlocks: 'Only `PASS` closes it; `FAIL` remains open for the remaining paper-review round; `UNKNOWN` blocks implementation handoff.',
|
|
65
|
+
scopeReturnsToC1: 'A repair that adds user semantics or scope returns to C1.',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const ADAPTIVE_COVERAGE_PROFILE_HEADER = [
|
|
69
|
+
'ID', 'Outcome', 'Change kinds', 'Material surfaces', 'Ambiguity/action',
|
|
70
|
+
'Risk/evidence', 'Required proof',
|
|
71
|
+
];
|
|
72
|
+
const ADAPTIVE_COVERAGE_PROFILE_ROW = [
|
|
73
|
+
'CP-01', '<externally observable outcome>', '<all kinds>', '<all material surfaces>',
|
|
74
|
+
'<state + action>', '<level + evidence>', '<source/installed/live set>',
|
|
75
|
+
];
|
|
76
|
+
const ADAPTIVE_COVERAGE_AMBIGUITY_ROWS = [
|
|
77
|
+
['State', 'Required action'],
|
|
78
|
+
['`none`', 'proceed'],
|
|
79
|
+
['`examples-needed`', 'add two or three examples only for an already decided rule; promote to `decision-needed` if an example changes observable behavior'],
|
|
80
|
+
['`decision-needed`', 'ask the user at C1/C2 and keep affected tasks blocked'],
|
|
81
|
+
['`design-needed`', 'after user-owned decisions settle, route material competing technical designs through Brainstorm'],
|
|
82
|
+
];
|
|
83
|
+
const ADAPTIVE_REVIEWER_ROWS = [
|
|
84
|
+
['Groups', 'Reviewers', 'Roles', 'Claim budget'],
|
|
85
|
+
['1-2', '2', 'Fact Checker plus all matching material lenses', 'about 5 per group'],
|
|
86
|
+
['3-5', '3', 'Fact Checker plus all matching material lenses', 'about 10 per group'],
|
|
87
|
+
['6+', '4', 'Fact Checker plus all matching material lenses', 'at least 15 total'],
|
|
88
|
+
];
|
|
89
|
+
const ADAPTIVE_COVERAGE_CLAUSES = {
|
|
90
|
+
riskFirst: 'Classify material risk before choosing a workflow; user wording never lowers an observed floor.',
|
|
91
|
+
criticalFloor: '`critical`: auth/secrets/privacy; destructive/irreversible work or possible data loss/corruption; money/privilege/safety; production-state mutation.',
|
|
92
|
+
elevatedFloor: '`elevated`: cross-component contracts, compatibility, concurrency, external integration, or installed/runtime behavior.',
|
|
93
|
+
frontmatterGate: 'skip only when a change is clear, isolated, reversible, routine, and likely limited to one or two files.',
|
|
94
|
+
directGate: 'Work directly only when the cause and change are clear, isolated, reversible,\n`routine`, and likely limited to one or two files.',
|
|
95
|
+
splitRoute: 'Split three or more independent\nsubsystems; otherwise use one Specs packet for any material work that does not qualify for direct work or Brainstorm-only exploration.',
|
|
96
|
+
independentSubsystem: 'A subsystem is independent only when its outcome, boundary, and verification/deployment path can move through the lifecycle separately.',
|
|
97
|
+
profileAuthority: 'For a Specs route, `plan.md` owns one `## Coverage profile` row per externally observable outcome; direct and Brainstorm-only routes do not persist it.',
|
|
98
|
+
openKinds: 'Change kinds are multi-valued (`add`, `modify`, `fix`, `refactor`, `remove`, `migrate`, `integrate`), and unfamiliar kinds or surfaces use `other:<verbatim>` rather than disappearing.',
|
|
99
|
+
scopedUnion: 'Each task references its CP IDs; authoring, review, edge, and proof obligations union only inside affected rows/tasks.',
|
|
100
|
+
profileRederivation: 'Rederive affected CP rows after any accepted scope, outcome, criteria, ownership, dependency, risk, or proof delta before task status.',
|
|
101
|
+
plannedProof: '`Required proof` is a planned level set, not execution\nevidence: known but unrun proof may be `pending`; `UNKNOWN` reachability blocks\n`pending`; missing, failed, or unavailable required evidence blocks `done`/C3.',
|
|
102
|
+
proofSeparation: 'Levels stay separate and never promote one another.',
|
|
103
|
+
liveLimit: 'Source/static checks prove the written contract, not live-model adherence.',
|
|
104
|
+
specMakerAuthority: 'they are the canonical risk and coverage authority. Do not duplicate\ntheir taxonomy here.',
|
|
105
|
+
specMakerAmbiguity: 'Apply the canonical ambiguity action; examples never decide observable behavior.',
|
|
106
|
+
specMakerRoute: 'Apply their risk-first route before C1 and stop when the\nrequest qualifies for direct work; hand off when it requires Brainstorm-only exploration.',
|
|
107
|
+
reviewRisk: 'Keep Fact Checker as the baseline. Assign every remaining material CP risk to a\nnamed reviewer lens; a critical row includes both relevant security-adversary and\nfailure-mode coverage, and nonmaterial lenses are not added.',
|
|
108
|
+
reviewCapacity: 'Reviewer count is fixed by the table, not lens count. Give each reviewer a distinct primary lens; when material lenses exceed reviewers, combine related named lenses on one reviewer and keep every material lens assigned.',
|
|
109
|
+
};
|
|
11
110
|
const REQUIRED_PAYLOAD = [
|
|
12
111
|
'bin/install.js',
|
|
13
112
|
'src/claude/migration-manifest.json',
|
|
@@ -22,6 +121,29 @@ const REQUIRED_PAYLOAD = [
|
|
|
22
121
|
'src/claude/scripts/validate-spec-output.cjs',
|
|
23
122
|
'src/claude/scripts/spec-authoring-validation.cjs',
|
|
24
123
|
'src/claude/scripts/spec-authoring-digest.cjs',
|
|
124
|
+
'src/claude/scripts/generate-skill-catalog.cjs',
|
|
125
|
+
'src/claude/rules/process-management.md',
|
|
126
|
+
'src/claude/rules/review-audit-self-decision.md',
|
|
127
|
+
'src/claude/rules/skill-domain-routing.md',
|
|
128
|
+
'src/claude/rules/skill-workflow-routing.md',
|
|
129
|
+
'src/claude/skills/route/SKILL.md',
|
|
130
|
+
'src/claude/skills/route/references/task-taxonomy.md',
|
|
131
|
+
'src/claude/skills/route/references/chaining-patterns.md',
|
|
132
|
+
'src/claude/skills/route/references/agent-timing.md',
|
|
133
|
+
'src/claude/skills/brainstorm/SKILL.md',
|
|
134
|
+
'src/claude/skills/brainstorm/references/question-framework.md',
|
|
135
|
+
'src/claude/agents/brainstormer.md',
|
|
136
|
+
'src/claude/skills/research/SKILL.md',
|
|
137
|
+
'src/claude/agents/researcher.md',
|
|
138
|
+
'src/claude/skills/loop/SKILL.md',
|
|
139
|
+
'src/claude/skills/loop/references/bounded-loop-protocol.md',
|
|
140
|
+
'src/claude/skills/loop/references/metric-and-guard-contract.md',
|
|
141
|
+
'src/claude/skills/docs/SKILL.md',
|
|
142
|
+
'src/claude/skills/docx/SKILL.md',
|
|
143
|
+
'src/claude/skills/pdf/SKILL.md',
|
|
144
|
+
'src/claude/skills/pptx/SKILL.md',
|
|
145
|
+
'src/claude/skills/xlsx/SKILL.md',
|
|
146
|
+
'src/claude/skills/ai-multimodal/SKILL.md',
|
|
25
147
|
];
|
|
26
148
|
const FORBIDDEN_PAYLOAD = [
|
|
27
149
|
/(^|\/)\.logs(\/|$)/,
|
|
@@ -31,11 +153,96 @@ const FORBIDDEN_PAYLOAD = [
|
|
|
31
153
|
/\.pyc$/,
|
|
32
154
|
/(^|\/)\.(?:cache|state|tmp)(\/|$)/,
|
|
33
155
|
/^src\/\.codex(?:\/|$)/,
|
|
156
|
+
/^src\/claude\/skills\/(?:backend-development|frontend-development|frontend-design|mobile-development|devops|react-best-practices)(?:\/|$)/,
|
|
34
157
|
];
|
|
35
158
|
const RUNTIMES = {
|
|
36
|
-
claude: {
|
|
37
|
-
|
|
159
|
+
claude: {
|
|
160
|
+
root: '.claude', rules: '.claude/hooks/rules.cjs', specs: '.claude/skills/specs',
|
|
161
|
+
specMaker: '.claude/agents/spec-maker.md',
|
|
162
|
+
manifest: '.claude/cafekit-manifest.json', templatesManifestPath: 'skills/specs/references/templates.md'
|
|
163
|
+
},
|
|
164
|
+
codex: {
|
|
165
|
+
root: '.codex', rules: '.codex/hooks/rules.cjs', specs: '.agents/skills/specs',
|
|
166
|
+
specMaker: '.codex/agents/spec_maker.toml',
|
|
167
|
+
manifest: '.codex/cafekit-manifest.json', templatesManifestPath: '.agents/skills/specs/references/templates.md'
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
const ADAPTIVE_SOURCE_RELATIVES = {
|
|
171
|
+
skill: 'src/claude/skills/specs/SKILL.md',
|
|
172
|
+
specMaker: 'src/claude/agents/spec-maker.md',
|
|
173
|
+
templates: 'src/claude/skills/specs/references/templates.md',
|
|
174
|
+
review: 'src/claude/skills/specs/references/review.md',
|
|
175
|
+
};
|
|
176
|
+
const BRAINSTORM_SOURCE_RELATIVES = {
|
|
177
|
+
skill: 'src/claude/skills/brainstorm/SKILL.md',
|
|
178
|
+
framework: 'src/claude/skills/brainstorm/references/question-framework.md',
|
|
179
|
+
agent: 'src/claude/agents/brainstormer.md',
|
|
180
|
+
};
|
|
181
|
+
const RESEARCH_LOOP_SOURCE_RELATIVES = {
|
|
182
|
+
research: 'src/claude/skills/research/SKILL.md',
|
|
183
|
+
agent: 'src/claude/agents/researcher.md',
|
|
184
|
+
loop: 'src/claude/skills/loop/SKILL.md',
|
|
185
|
+
protocol: 'src/claude/skills/loop/references/bounded-loop-protocol.md',
|
|
186
|
+
metric: 'src/claude/skills/loop/references/metric-and-guard-contract.md',
|
|
187
|
+
workflow: 'src/claude/rules/skill-workflow-routing.md',
|
|
188
|
+
domain: 'src/claude/rules/skill-domain-routing.md',
|
|
189
|
+
};
|
|
190
|
+
const HOTFIX_SOURCE_RELATIVES = {
|
|
191
|
+
skill: 'src/claude/skills/hotfix/SKILL.md',
|
|
192
|
+
review: 'src/claude/skills/hotfix/references/review-cycle.md',
|
|
193
|
+
parallel: 'src/claude/skills/hotfix/references/parallel-patterns.md',
|
|
194
|
+
specialized: 'src/claude/skills/hotfix/references/workflow-specialized.md',
|
|
38
195
|
};
|
|
196
|
+
const REQUIRED_ADAPTIVE_BRAINSTORM_GROUPS = [
|
|
197
|
+
'adviser-gate-fallback',
|
|
198
|
+
'decision-brief',
|
|
199
|
+
'direct-precedence',
|
|
200
|
+
'evidence-semantics',
|
|
201
|
+
'leading-flags',
|
|
202
|
+
'lens-trigger-skip',
|
|
203
|
+
'no-persistence-dispatch',
|
|
204
|
+
'numeric-estimates',
|
|
205
|
+
'ordered-depth',
|
|
206
|
+
'pre-tool-authority-redaction',
|
|
207
|
+
];
|
|
208
|
+
const ADAPTIVE_BRAINSTORM_INSTALLED_RULES = [
|
|
209
|
+
{ group: 'direct-precedence', mutations: [
|
|
210
|
+
{ from: 'Route Direct first, then\napply controls only to requests that remain in Brainstorm.', to: 'Apply controls before Direct classification.', clause: 'Route Direct first, then apply controls only to requests that remain in Brainstorm.' },
|
|
211
|
+
] },
|
|
212
|
+
{ group: 'ordered-depth', mutations: [
|
|
213
|
+
{ from: 'With no Deep signal, use Standard.', to: 'Deep is always the default.', clause: 'With no Deep signal, use Standard. `--deep` raises Standard to Deep.' },
|
|
214
|
+
] },
|
|
215
|
+
{ group: 'leading-flags', mutations: [
|
|
216
|
+
{ from: 'Parse controls only from the leading consecutive token segment.', to: 'Parse flag-like tokens anywhere.', clause: 'Parse controls only from the leading consecutive token segment.' },
|
|
217
|
+
{ from: '`--deep`, `--visual`, and `--advice` in any order, each at most once.', to: '`--deep`, `--visual`, and `--advice` may repeat.', clause: '`--deep`, `--visual`, and `--advice` in any order, each at most once.' },
|
|
218
|
+
{ from: '`--` ends\nthe control segment.', to: '`--` is treated as another control.', clause: '`--` ends the control segment.' },
|
|
219
|
+
{ from: 'An unknown or duplicate `--*` inside the leading segment returns usage', to: 'An unknown or duplicate `--*` is ignored', clause: 'An unknown or duplicate `--*` inside the leading segment returns usage and performs no scout, question, tool call, write, or workflow action.' },
|
|
220
|
+
] },
|
|
221
|
+
{ group: 'lens-trigger-skip', mutations: [
|
|
222
|
+
{ from: 'failure isolation for partial or cascading failure\nacross boundaries', to: 'generic failure notes', clause: 'failure isolation for partial or cascading failure across boundaries' },
|
|
223
|
+
] },
|
|
224
|
+
{ group: 'evidence-semantics', mutations: [
|
|
225
|
+
{ from: 'Missing evidence\nforces feasibility `unknown` and confidence `low`.', to: 'Missing evidence permits a confident guess.', clause: 'Missing evidence forces feasibility `unknown` and confidence `low`.' },
|
|
226
|
+
] },
|
|
227
|
+
{ group: 'numeric-estimates', mutations: [
|
|
228
|
+
{ from: 'A numeric estimate requires\nrange, unit, basis, evidence, and assumptions; otherwise report `unknown`.', to: 'A numeric estimate may be a best-effort number.', clause: 'A numeric estimate requires range, unit, basis, evidence, and assumptions; otherwise report `unknown`.' },
|
|
229
|
+
] },
|
|
230
|
+
{ group: 'pre-tool-authority-redaction', mutations: [
|
|
231
|
+
{ from: 'Before an\nexternal visual tool or adviser handoff, minimize context and redact secrets,\ncredentials, private keys, access tokens, and unnecessary PII.', to: 'Forward full context to every external tool and adviser.', clause: 'Before an external visual tool or adviser handoff, minimize context and redact secrets, credentials, private keys, access tokens, and unnecessary PII.' },
|
|
232
|
+
{ from: '`--visual` may present inline Mermaid or ASCII for any non-direct analysis and\nfalls back to equivalent text when rendering is unavailable.', to: '`--visual` fails when rendering is unavailable.', clause: '`--visual` may present inline Mermaid or ASCII for any non-direct analysis and falls back to equivalent text when rendering is unavailable.' },
|
|
233
|
+
{ from: 'Durable or external\nrendering requires explicit user authority before invocation.', to: 'Durable or external rendering may run without consent.', clause: 'Durable or external rendering requires explicit user authority before invocation.' },
|
|
234
|
+
] },
|
|
235
|
+
{ group: 'adviser-gate-fallback', mutations: [
|
|
236
|
+
{ from: '`--advice` invokes\n`brainstormer` only after the material-choice gate;', to: '`--advice` invokes `brainstormer` before routing;', clause: '`--advice` invokes `brainstormer` only after the material-choice gate;' },
|
|
237
|
+
{ from: 'if advice is unavailable or\nfails, label it unavailable and continue with controller analysis.', to: 'if advice is unavailable, stop the workflow.', clause: 'if advice is unavailable or fails, label it unavailable and continue with controller analysis.' },
|
|
238
|
+
] },
|
|
239
|
+
{ group: 'decision-brief', mutations: [
|
|
240
|
+
{ from: 'The first section records target\nidentity, current source revision and worktree state or `[UNVERIFIED]`, an\nevidence-as-of value, and what change invalidates the brief.', to: 'The handoff has no revision or freshness binding.', clause: 'The first section records target identity, current source revision and worktree state or `[UNVERIFIED]`, an evidence-as-of value, and what change invalidates the brief.' },
|
|
241
|
+
] },
|
|
242
|
+
{ group: 'no-persistence-dispatch', mutations: [
|
|
243
|
+
{ from: 'Neither overlay\nwrites, approves, persists, dispatches, or completes work.', to: 'Overlays may persist, approve, dispatch, and complete work.', clause: 'Neither overlay writes, approves, persists, dispatches, or completes work.' },
|
|
244
|
+
] },
|
|
245
|
+
];
|
|
39
246
|
|
|
40
247
|
function npmPack(args, cwd) {
|
|
41
248
|
const cache = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-npm-pack-cache-'));
|
|
@@ -122,8 +329,8 @@ function installPacked(tarball, root, runtimeClosure) {
|
|
|
122
329
|
return installer;
|
|
123
330
|
}
|
|
124
331
|
|
|
125
|
-
function runInstaller(installer, root, platforms, lang) {
|
|
126
|
-
const args = ['--platform', platforms.join(','), '--yes'];
|
|
332
|
+
function runInstaller(installer, root, platforms, lang, extraArgs = []) {
|
|
333
|
+
const args = ['--platform', platforms.join(','), '--yes', ...extraArgs];
|
|
127
334
|
if (lang) args.push('--lang', lang);
|
|
128
335
|
const result = spawnSync(process.execPath, [installer, ...args], {
|
|
129
336
|
cwd: root,
|
|
@@ -131,6 +338,1227 @@ function runInstaller(installer, root, platforms, lang) {
|
|
|
131
338
|
env: { ...process.env, HOME: path.join(root, 'home'), PATH: '/usr/bin:/bin' },
|
|
132
339
|
});
|
|
133
340
|
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function installedRouteFiles(project, platform) {
|
|
345
|
+
const runtimeRoot = platform === 'codex' ? '.codex' : '.claude';
|
|
346
|
+
const root = path.join(project, platform === 'codex' ? '.agents/skills/route' : '.claude/skills/route');
|
|
347
|
+
return {
|
|
348
|
+
skill: path.join(root, 'SKILL.md'),
|
|
349
|
+
taxonomy: path.join(root, 'references/task-taxonomy.md'),
|
|
350
|
+
chaining: path.join(root, 'references/chaining-patterns.md'),
|
|
351
|
+
timing: path.join(root, 'references/agent-timing.md'),
|
|
352
|
+
workflow: path.join(project, runtimeRoot, 'rules/skill-workflow-routing.md'),
|
|
353
|
+
domain: path.join(project, runtimeRoot, 'rules/skill-domain-routing.md'),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function routeProjectionIssues(files) {
|
|
358
|
+
const text = Object.fromEntries(Object.entries(files).map(([key, file]) => [
|
|
359
|
+
key, fs.readFileSync(file, 'utf8').replace(/\s+/g, ' '),
|
|
360
|
+
]));
|
|
361
|
+
const issues = new Set();
|
|
362
|
+
const requires = (issue, clauses) => {
|
|
363
|
+
if (clauses.some(([source, clause]) => !text[source].includes(clause))) issues.add(issue);
|
|
364
|
+
};
|
|
365
|
+
requires('direct-path', [
|
|
366
|
+
['skill', 'invoke that skill directly'],
|
|
367
|
+
['skill', 'use it directly without constructing a chain'],
|
|
368
|
+
['skill', 'Do not create a route chain or spawn agents merely to answer it'],
|
|
369
|
+
]);
|
|
370
|
+
requires('classification', [
|
|
371
|
+
['skill', 'final deliverable'], ['skill', 'highest-link risk'],
|
|
372
|
+
['skill', 'number of material domains'],
|
|
373
|
+
]);
|
|
374
|
+
requires('link-contract', [
|
|
375
|
+
['chaining', '**Entry:**'], ['chaining', '**Exit:**'], ['chaining', '**Owner:** exactly one'],
|
|
376
|
+
]);
|
|
377
|
+
requires('failure-stop', [
|
|
378
|
+
['chaining', '(link, owner, normalized cause)'],
|
|
379
|
+
['chaining', 'Two failures with the same failure key stop the chain'],
|
|
380
|
+
['chaining', 'Different normalized causes do not share a key'],
|
|
381
|
+
]);
|
|
382
|
+
requires('collapse-and-detour', [
|
|
383
|
+
['skill', 'remove every link whose output is already evidenced'],
|
|
384
|
+
['chaining', 'preserve its valid exit evidence, normalize the root cause, and choose one bounded detour'],
|
|
385
|
+
]);
|
|
386
|
+
requires('delegation-contract', [
|
|
387
|
+
['timing', '**Outcome** — observable result'],
|
|
388
|
+
['timing', '**Scope** — owned files, systems, or questions'],
|
|
389
|
+
['timing', '**Inputs** — current evidence and prerequisite artifacts'],
|
|
390
|
+
['timing', '**Constraints** — safety, compatibility, authority, and non-goals'],
|
|
391
|
+
['timing', '**Acceptance** — proof required for completion'],
|
|
392
|
+
['timing', '**Handoff** — expected returned artifact and destination'],
|
|
393
|
+
['timing', '**Status vocabulary** — `DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT`'],
|
|
394
|
+
]);
|
|
395
|
+
requires('agent-fallback', [
|
|
396
|
+
['timing', 'If the preferred agent is absent'],
|
|
397
|
+
['timing', 'never synthesize a role'],
|
|
398
|
+
['timing', 'DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT'],
|
|
399
|
+
]);
|
|
400
|
+
requires('authority', [
|
|
401
|
+
['skill', 'never expand it'],
|
|
402
|
+
['skill', 'diagnosis does not authorize repair'],
|
|
403
|
+
['skill', 'does not authorize commit, push, deploy, publish, or release'],
|
|
404
|
+
]);
|
|
405
|
+
requires('risk-gates', [
|
|
406
|
+
['skill', 'require independent review and user confirmation before advancing past the high-risk gate'],
|
|
407
|
+
]);
|
|
408
|
+
requires('proof-boundary', [
|
|
409
|
+
['skill', 'remains `[UNPROVEN]` until a separate host run observes it'],
|
|
410
|
+
]);
|
|
411
|
+
requires('rule-direct-path', [
|
|
412
|
+
['workflow', 'If the user names a valid installed skill, use it directly'],
|
|
413
|
+
['workflow', 'If one obvious low-risk installed skill covers the intent, use it directly'],
|
|
414
|
+
['workflow', 'do not invoke Route or agents for ceremony'],
|
|
415
|
+
]);
|
|
416
|
+
requires('rule-live-catalog', [
|
|
417
|
+
['workflow', 'Resolve every abstract link against the current runtime catalog'],
|
|
418
|
+
['domain', 'examples below are intent hints, not a copied installed inventory'],
|
|
419
|
+
]);
|
|
420
|
+
requires('rule-installed-only', [
|
|
421
|
+
['domain', 'document/artifact work | use only a matching installed optional capability'],
|
|
422
|
+
['domain', 'Never infer an optional document capability from this rule'],
|
|
423
|
+
]);
|
|
424
|
+
requires('rule-duplicate', [
|
|
425
|
+
['domain', 'do not auto-route; require explicit user disambiguation'],
|
|
426
|
+
]);
|
|
427
|
+
requires('authority', [
|
|
428
|
+
['workflow', 'Diagnosis does not authorize repair'],
|
|
429
|
+
['workflow', 'implementation does not authorize commit, push, deploy, publish, or release'],
|
|
430
|
+
['workflow', "no route expands the user's existing authority"],
|
|
431
|
+
]);
|
|
432
|
+
|
|
433
|
+
const corpus = Object.values(text).join(' ').toLowerCase();
|
|
434
|
+
const issueIf = (issue, pattern) => { if (pattern.test(corpus)) issues.add(issue); };
|
|
435
|
+
issueIf('authority', /(?:successful review permits push|implementation authorizes (?:push|deploy)|diagnosis authorizes repair)/);
|
|
436
|
+
issueIf('authority', /(?<!no )(?:live catalog|route)(?:(?!\b(?:never|not|cannot|can't)\b).){0,40}(?:grants|expands)(?:(?!\b(?:no|never|not|cannot|can't)\b).){0,30}(?:mutation|delivery|user )?authority/);
|
|
437
|
+
issueIf('rule-direct-path', /always invoke route for explicit installed skills[^.]*obvious low-risk[^.]*factual/);
|
|
438
|
+
issueIf('rule-live-catalog', /(?<!never )treat examples as installed inventory/);
|
|
439
|
+
issueIf('rule-installed-only', /(?<!never )invoke (?:the )?(?:docs|document) capability even when (?:absent|not installed|unavailable)/);
|
|
440
|
+
issueIf('rule-duplicate', /(?<!never )automatically (?:choose|select|route to) the first duplicate/);
|
|
441
|
+
return [...issues].sort();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function markdownTableUnderHeading(content, heading) {
|
|
445
|
+
const lines = markdownSectionUnderHeading(content, heading).split('\n');
|
|
446
|
+
const tableStart = lines.findIndex((line) => line.trim().startsWith('|'));
|
|
447
|
+
if (tableStart < 0) return [];
|
|
448
|
+
const rows = [];
|
|
449
|
+
for (let index = tableStart; index < lines.length && lines[index].trim().startsWith('|'); index += 1) {
|
|
450
|
+
const cells = lines[index].split('|').slice(1, -1).map((cell) => cell.trim());
|
|
451
|
+
if (cells.every((cell) => /^:?-+:?$/.test(cell))) continue;
|
|
452
|
+
rows.push(cells);
|
|
453
|
+
}
|
|
454
|
+
return rows;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function markdownSectionUnderHeading(content, heading) {
|
|
458
|
+
const lines = String(content).split('\n');
|
|
459
|
+
const headingIndex = lines.findIndex((line) => line.trim() === `## ${heading}`);
|
|
460
|
+
if (headingIndex < 0) return '';
|
|
461
|
+
const nextHeadingIndex = lines.findIndex(
|
|
462
|
+
(line, index) => index > headingIndex && /^##\s+/.test(line)
|
|
463
|
+
);
|
|
464
|
+
return lines.slice(headingIndex + 1, nextHeadingIndex < 0 ? undefined : nextHeadingIndex).join('\n');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function normalizeMarkdownWhitespace(content) {
|
|
468
|
+
return String(content).replace(/\s+/g, ' ').trim();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function markdownBetweenHeadings(content, startHeading, endHeading) {
|
|
472
|
+
const value = String(content);
|
|
473
|
+
const startMarker = `## ${startHeading}`;
|
|
474
|
+
const endMarker = `## ${endHeading}`;
|
|
475
|
+
const startIndex = value.indexOf(startMarker);
|
|
476
|
+
const endIndex = value.indexOf(endMarker, startIndex + startMarker.length);
|
|
477
|
+
if (startIndex < 0 || endIndex < 0) return '';
|
|
478
|
+
return value.slice(startIndex + startMarker.length, endIndex);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function parseGeneratedTomlString(content, key) {
|
|
482
|
+
const matches = String(content).match(new RegExp(`^${key} = (.+)$`, 'gm')) || [];
|
|
483
|
+
assert.equal(matches.length, 1, `expected one TOML key: ${key}`);
|
|
484
|
+
return JSON.parse(matches[0].slice(`${key} = `.length));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function rewriteGeneratedTomlString(content, key, value) {
|
|
488
|
+
const matches = String(content).match(new RegExp(`^${key} = (.+)$`, 'gm')) || [];
|
|
489
|
+
assert.equal(matches.length, 1, `expected one TOML key to rewrite: ${key}`);
|
|
490
|
+
return String(content).replace(matches[0], `${key} = ${JSON.stringify(value)}`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function implementationReadinessContractIssues(input) {
|
|
494
|
+
const keys = input && typeof input === 'object' && !Array.isArray(input)
|
|
495
|
+
? Object.keys(input).sort()
|
|
496
|
+
: [];
|
|
497
|
+
if (keys.join(',') !== 'review,templates'
|
|
498
|
+
|| typeof input.templates !== 'string' || typeof input.review !== 'string') {
|
|
499
|
+
throw new TypeError('implementation-readiness checker expects exactly templates and review UTF-8 strings');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const issues = new Set();
|
|
503
|
+
const authoring = markdownSectionUnderHeading(
|
|
504
|
+
input.templates,
|
|
505
|
+
'No-invention and conditional boundary contracts'
|
|
506
|
+
);
|
|
507
|
+
if (!authoring.includes(IMPLEMENTATION_READINESS_CLAUSES.noInvention)) {
|
|
508
|
+
issues.add('no-invention');
|
|
509
|
+
}
|
|
510
|
+
const contradictoryNoInvention = /\b(?:exception|however)\b.{0,160}\bimplementation handoff\b.{0,80}\b(?:may|can)\s+(?:proceed|continue)\b.{0,160}\bunresolved\b/i;
|
|
511
|
+
if (contradictoryNoInvention.test(normalizeMarkdownWhitespace(authoring))) {
|
|
512
|
+
issues.add('no-invention');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const boundaryTable = markdownTableUnderHeading(
|
|
516
|
+
input.templates,
|
|
517
|
+
'No-invention and conditional boundary contracts'
|
|
518
|
+
);
|
|
519
|
+
if (!authoring.includes(IMPLEMENTATION_READINESS_CLAUSES.materialDefinition)
|
|
520
|
+
|| !authoring.includes(IMPLEMENTATION_READINESS_CLAUSES.exactBoundaryChoices)
|
|
521
|
+
|| IMPLEMENTATION_READINESS_BOUNDARY_ROWS.length !== 9
|
|
522
|
+
|| !boundaryTableHasRequiredRows(boundaryTable)) {
|
|
523
|
+
issues.add('boundary-contract');
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const proof = markdownSectionUnderHeading(input.templates, 'Verification Plan');
|
|
527
|
+
const proofPlanLines = proof.split('\n').map((line) => line.trim()).filter((line) => line.startsWith('- '));
|
|
528
|
+
const normalizedProofContract = normalizeMarkdownWhitespace(markdownBetweenHeadings(
|
|
529
|
+
input.templates,
|
|
530
|
+
'Verification Plan',
|
|
531
|
+
'Canonical inline Receipt'
|
|
532
|
+
));
|
|
533
|
+
const normalizedProofClauses = [
|
|
534
|
+
IMPLEMENTATION_READINESS_CLAUSES.proofTrace,
|
|
535
|
+
IMPLEMENTATION_READINESS_CLAUSES.namedProbeOwnership,
|
|
536
|
+
IMPLEMENTATION_READINESS_CLAUSES.proofLevelSeparation,
|
|
537
|
+
IMPLEMENTATION_READINESS_CLAUSES.disposableTemplateControls,
|
|
538
|
+
IMPLEMENTATION_READINESS_CLAUSES.proofLevelMapping,
|
|
539
|
+
].map(normalizeMarkdownWhitespace);
|
|
540
|
+
const reviewNegativeControls = normalizeMarkdownWhitespace(
|
|
541
|
+
markdownSectionUnderHeading(input.review, 'B2 — fresh-context red team')
|
|
542
|
+
);
|
|
543
|
+
if (JSON.stringify(proofPlanLines) !== JSON.stringify(IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines)
|
|
544
|
+
|| normalizedProofClauses.some((clause) => !normalizedProofContract.includes(clause))
|
|
545
|
+
|| !reviewNegativeControls.includes(normalizeMarkdownWhitespace(
|
|
546
|
+
IMPLEMENTATION_READINESS_CLAUSES.disposableReviewControls
|
|
547
|
+
))) {
|
|
548
|
+
issues.add('proof-chain');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const failureGuidance = markdownSectionUnderHeading(input.templates, 'Twelve edge-case dimensions');
|
|
552
|
+
if (!failureGuidance.includes(IMPLEMENTATION_READINESS_CLAUSES.failureSemantics)) {
|
|
553
|
+
issues.add('failure-semantics');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const evidenceRules = markdownSectionUnderHeading(input.review, 'B1 — evidence rule');
|
|
557
|
+
if (!evidenceRules.includes(IMPLEMENTATION_READINESS_CLAUSES.privacyIdentifiers)) {
|
|
558
|
+
issues.add('privacy-identifiers');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const closure = markdownSectionUnderHeading(input.review, 'Accepted-repair closure');
|
|
562
|
+
const normalizedClosure = normalizeMarkdownWhitespace(closure);
|
|
563
|
+
const closureHeader = markdownTableUnderHeading(input.review, 'Accepted-repair closure')[0] || [];
|
|
564
|
+
if (JSON.stringify(closureHeader) !== JSON.stringify([
|
|
565
|
+
'ID', 'Decision', 'Original counterexample', 'Repaired at', 'Proved at', 'Replay', 'Closure',
|
|
566
|
+
])
|
|
567
|
+
|| !normalizedClosure.includes(normalizeMarkdownWhitespace(IMPLEMENTATION_READINESS_CLAUSES.freshReplay))
|
|
568
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.distinctRepairProof)
|
|
569
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.closureTransition)
|
|
570
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.unknownBlocks)
|
|
571
|
+
|| !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.scopeReturnsToC1)) {
|
|
572
|
+
issues.add('repair-closure');
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return [...issues].sort();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function adaptiveCoverageContractIssues(input) {
|
|
579
|
+
const expectedKeys = ['review', 'skill', 'specMaker', 'templates'];
|
|
580
|
+
const keys = input && typeof input === 'object' && !Array.isArray(input)
|
|
581
|
+
? Object.keys(input).sort()
|
|
582
|
+
: [];
|
|
583
|
+
if (JSON.stringify(keys) !== JSON.stringify(expectedKeys)
|
|
584
|
+
|| expectedKeys.some((key) => typeof input[key] !== 'string')) {
|
|
585
|
+
throw new TypeError('adaptive-coverage checker expects skill, specMaker, templates, and review UTF-8 strings');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const issues = new Set();
|
|
589
|
+
const skill = normalizeMarkdownWhitespace(input.skill);
|
|
590
|
+
const templates = normalizeMarkdownWhitespace(input.templates);
|
|
591
|
+
const review = normalizeMarkdownWhitespace(input.review);
|
|
592
|
+
const specMaker = normalizeMarkdownWhitespace(input.specMaker);
|
|
593
|
+
const has = (source, clause) => source.includes(normalizeMarkdownWhitespace(clause));
|
|
594
|
+
|
|
595
|
+
const riskClauses = [
|
|
596
|
+
ADAPTIVE_COVERAGE_CLAUSES.riskFirst,
|
|
597
|
+
ADAPTIVE_COVERAGE_CLAUSES.criticalFloor,
|
|
598
|
+
ADAPTIVE_COVERAGE_CLAUSES.elevatedFloor,
|
|
599
|
+
ADAPTIVE_COVERAGE_CLAUSES.frontmatterGate,
|
|
600
|
+
ADAPTIVE_COVERAGE_CLAUSES.directGate,
|
|
601
|
+
ADAPTIVE_COVERAGE_CLAUSES.splitRoute,
|
|
602
|
+
ADAPTIVE_COVERAGE_CLAUSES.independentSubsystem,
|
|
603
|
+
];
|
|
604
|
+
const riskDowngrade = /\buser\b.{0,80}\b(?:may|can)\b.{0,80}\blower\b.{0,40}\brisk\b/i;
|
|
605
|
+
const riskyDirectOverride = /\b(?:exception|even if|regardless)\b.{0,160}\b(?:auth|secret|privacy|destructive|irreversible|data loss|corruption|production[- ]state|critical)\b.{0,160}\b(?:direct|work directly|go direct)\b/i;
|
|
606
|
+
if (riskClauses.some((clause) => !has(skill, clause))
|
|
607
|
+
|| riskDowngrade.test(skill) || riskyDirectOverride.test(skill)) {
|
|
608
|
+
issues.add('risk-first-routing');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const profileTable = markdownTableUnderHeading(input.templates, 'Coverage profile');
|
|
612
|
+
const profileHeadingCount = (input.templates.match(/^## Coverage profile\s*$/gm) || []).length;
|
|
613
|
+
const taskReferencesCoverage = /## Coverage\s*\n- <exact `CP-NN` IDs owned by this task>/.test(input.templates);
|
|
614
|
+
if (JSON.stringify(profileTable[0] || []) !== JSON.stringify(ADAPTIVE_COVERAGE_PROFILE_HEADER)
|
|
615
|
+
|| profileHeadingCount !== 1 || profileTable.length !== 2
|
|
616
|
+
|| JSON.stringify(profileTable[1]) !== JSON.stringify(ADAPTIVE_COVERAGE_PROFILE_ROW)
|
|
617
|
+
|| !taskReferencesCoverage
|
|
618
|
+
|| !has(templates, ADAPTIVE_COVERAGE_CLAUSES.profileAuthority)
|
|
619
|
+
|| !has(templates, ADAPTIVE_COVERAGE_CLAUSES.openKinds)) {
|
|
620
|
+
issues.add('coverage-profile-shape');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const ambiguityTable = markdownTableUnderHeading(input.templates, 'Example Mapping rule');
|
|
624
|
+
if (JSON.stringify(ambiguityTable) !== JSON.stringify(ADAPTIVE_COVERAGE_AMBIGUITY_ROWS)
|
|
625
|
+
|| !templates.includes('retention of 30 versus 90 days is `decision-needed`')) {
|
|
626
|
+
issues.add('ambiguity-actions');
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const globalCeremony = /\b(?:critical|security|failure|proof|review|edge|obligations?|lenses?)\b[^.!?\n]{0,120}\b(?:union|apply|spread|require)\w*\b[^.!?\n]{0,120}\b(?:all|every)\s+(?:cp\s+)?(?:rows?|outcomes?|tasks?)\b|\b(?:union|apply|spread)\w*\b[^.!?\n]{0,120}\b(?:all|every)\s+(?:cp\s+)?(?:rows?|outcomes?|tasks?)\b/i;
|
|
630
|
+
if (!has(templates, ADAPTIVE_COVERAGE_CLAUSES.scopedUnion)
|
|
631
|
+
|| globalCeremony.test(templates)
|
|
632
|
+
|| !review.includes('nonmaterial lenses are not added')) {
|
|
633
|
+
issues.add('scoped-coverage');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const rederiveSources = [templates, skill, specMaker, review];
|
|
637
|
+
if (!has(templates, ADAPTIVE_COVERAGE_CLAUSES.profileRederivation)
|
|
638
|
+
|| rederiveSources.some((source) => !/rederive affected cp rows?/.test(source.toLowerCase()))) {
|
|
639
|
+
issues.add('profile-lifecycle');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const statusMatrix = markdownTableUnderHeading(input.templates, 'Status matrix');
|
|
643
|
+
if (!has(templates, ADAPTIVE_COVERAGE_CLAUSES.plannedProof)
|
|
644
|
+
|| !has(templates, ADAPTIVE_COVERAGE_CLAUSES.proofSeparation)
|
|
645
|
+
|| !has(templates, IMPLEMENTATION_READINESS_CLAUSES.proofLevelMapping)
|
|
646
|
+
|| !has(skill, ADAPTIVE_COVERAGE_CLAUSES.liveLimit)
|
|
647
|
+
|| !statusMatrix.some((row) => row[0] === 'accepted finding open or `UNKNOWN` reachability' && row[1] === '`blocked`')) {
|
|
648
|
+
issues.add('proof-lifecycle');
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const reviewerRowsPresent = ADAPTIVE_REVIEWER_ROWS.every((row) =>
|
|
652
|
+
input.review.includes(`| ${row.join(' | ')} |`));
|
|
653
|
+
if (!has(review, ADAPTIVE_COVERAGE_CLAUSES.reviewRisk)
|
|
654
|
+
|| !has(review, ADAPTIVE_COVERAGE_CLAUSES.reviewCapacity)
|
|
655
|
+
|| !reviewerRowsPresent) {
|
|
656
|
+
issues.add('reviewer-routing');
|
|
657
|
+
}
|
|
658
|
+
if (!specMaker.includes('skills/specs/SKILL.md')
|
|
659
|
+
|| !specMaker.includes('skills/specs/references/templates.md')
|
|
660
|
+
|| !has(specMaker, ADAPTIVE_COVERAGE_CLAUSES.specMakerAuthority)
|
|
661
|
+
|| !has(specMaker, ADAPTIVE_COVERAGE_CLAUSES.specMakerAmbiguity)
|
|
662
|
+
|| !has(specMaker, ADAPTIVE_COVERAGE_CLAUSES.specMakerRoute)) {
|
|
663
|
+
issues.add('spec-maker-authority');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const boundaryTable = markdownTableUnderHeading(input.templates, 'No-invention and conditional boundary contracts');
|
|
667
|
+
if (!boundaryTableHasRequiredRows(boundaryTable)) {
|
|
668
|
+
issues.add('adaptive-boundary-lenses');
|
|
669
|
+
}
|
|
670
|
+
return [...issues].sort();
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function installedSpecsReadinessIssues(project, installedRoot, expectedRelative) {
|
|
674
|
+
assert.equal(path.isAbsolute(project), true, 'project root must be absolute');
|
|
675
|
+
assert.equal(path.isAbsolute(installedRoot), true, 'installed Specs root must be absolute');
|
|
676
|
+
const canonicalProject = fs.realpathSync(project);
|
|
677
|
+
const canonicalInstalled = fs.realpathSync(installedRoot);
|
|
678
|
+
const relative = path.relative(canonicalProject, canonicalInstalled);
|
|
679
|
+
assert.ok(relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative), 'installed Specs root must stay inside project');
|
|
680
|
+
assert.equal(canonicalInstalled, fs.realpathSync(path.join(canonicalProject, expectedRelative)), 'checker must use the native installed Specs root');
|
|
681
|
+
return implementationReadinessContractIssues({
|
|
682
|
+
templates: fs.readFileSync(path.join(canonicalInstalled, 'references/templates.md'), 'utf8'),
|
|
683
|
+
review: fs.readFileSync(path.join(canonicalInstalled, 'references/review.md'), 'utf8'),
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function assertInstalledSpecsReadiness(project, platform, expected = []) {
|
|
688
|
+
const runtime = RUNTIMES[platform];
|
|
689
|
+
assert.ok(runtime, `unknown platform: ${platform}`);
|
|
690
|
+
const installedRoot = path.join(project, runtime.specs);
|
|
691
|
+
const issues = installedSpecsReadinessIssues(project, installedRoot, runtime.specs);
|
|
692
|
+
assert.deepEqual(issues, expected, `${platform} installed Specs readiness issues`);
|
|
693
|
+
return issues;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function adaptiveInstalledPaths(project, platform) {
|
|
697
|
+
const runtime = RUNTIMES[platform];
|
|
698
|
+
assert.ok(runtime, `unknown platform: ${platform}`);
|
|
699
|
+
return {
|
|
700
|
+
skill: path.join(project, runtime.specs, 'SKILL.md'),
|
|
701
|
+
specMaker: path.join(project, runtime.specMaker),
|
|
702
|
+
templates: path.join(project, runtime.specs, 'references/templates.md'),
|
|
703
|
+
review: path.join(project, runtime.specs, 'references/review.md'),
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function installedAdaptiveCoverageSources(project, platform) {
|
|
708
|
+
const installedPaths = adaptiveInstalledPaths(project, platform);
|
|
709
|
+
const sources = Object.fromEntries(
|
|
710
|
+
Object.entries(installedPaths).map(([source, target]) => [source, fs.readFileSync(target, 'utf8')])
|
|
711
|
+
);
|
|
712
|
+
if (platform === 'codex') {
|
|
713
|
+
sources.specMaker = parseGeneratedTomlString(sources.specMaker, 'developer_instructions');
|
|
714
|
+
}
|
|
715
|
+
return sources;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function assertInstalledAdaptiveCoverage(project, platform, expected = []) {
|
|
719
|
+
const issues = adaptiveCoverageContractIssues(installedAdaptiveCoverageSources(project, platform));
|
|
720
|
+
assert.deepEqual(issues, expected, `${platform} installed adaptive Specs issues`);
|
|
721
|
+
return issues;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function assertPackedAdaptiveParity(project, platform) {
|
|
725
|
+
const installedPaths = adaptiveInstalledPaths(project, platform);
|
|
726
|
+
for (const [source, sourceRelative] of Object.entries(ADAPTIVE_SOURCE_RELATIVES)) {
|
|
727
|
+
const sourcePath = path.join(PACKAGE_ROOT, sourceRelative);
|
|
728
|
+
const sourceBytes = fs.readFileSync(sourcePath);
|
|
729
|
+
let expectedBytes = sourceBytes;
|
|
730
|
+
if (platform === 'codex') {
|
|
731
|
+
const sourceText = sourceBytes.toString('utf8');
|
|
732
|
+
expectedBytes = Buffer.from(source === 'specMaker'
|
|
733
|
+
? convertCodexAgentContent(sourceText, path.basename(sourcePath))
|
|
734
|
+
: normalizeCodexBody(sourceText, sourcePath));
|
|
735
|
+
}
|
|
736
|
+
assert.deepEqual(
|
|
737
|
+
fs.readFileSync(installedPaths[source]),
|
|
738
|
+
expectedBytes,
|
|
739
|
+
`${platform} installed ${source} must equal its exact production projection`
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
assertInstalledSpecsReadiness(project, platform);
|
|
743
|
+
assertInstalledAdaptiveCoverage(project, platform);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function assertPackedBrainstormParity(project, platform) {
|
|
747
|
+
const installedRelatives = platform === 'codex'
|
|
748
|
+
? {
|
|
749
|
+
skill: '.agents/skills/brainstorm/SKILL.md',
|
|
750
|
+
framework: '.agents/skills/brainstorm/references/question-framework.md',
|
|
751
|
+
agent: '.codex/agents/brainstormer.toml',
|
|
752
|
+
}
|
|
753
|
+
: {
|
|
754
|
+
skill: '.claude/skills/brainstorm/SKILL.md',
|
|
755
|
+
framework: '.claude/skills/brainstorm/references/question-framework.md',
|
|
756
|
+
agent: '.claude/agents/brainstormer.md',
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
for (const [source, sourceRelative] of Object.entries(BRAINSTORM_SOURCE_RELATIVES)) {
|
|
760
|
+
const sourcePath = path.join(PACKAGE_ROOT, sourceRelative);
|
|
761
|
+
const sourceText = fs.readFileSync(sourcePath, 'utf8');
|
|
762
|
+
const expected = platform === 'codex'
|
|
763
|
+
? (source === 'agent'
|
|
764
|
+
? convertCodexAgentContent(sourceText, path.basename(sourcePath))
|
|
765
|
+
: normalizeCodexBody(sourceText, sourcePath))
|
|
766
|
+
: sourceText;
|
|
767
|
+
const installedPath = path.join(project, installedRelatives[source]);
|
|
768
|
+
assert.equal(
|
|
769
|
+
fs.readFileSync(installedPath, 'utf8'),
|
|
770
|
+
expected,
|
|
771
|
+
`${platform} packed Brainstorm ${source} must equal its exact production projection`
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function packedResearchLoopPaths(project, platform) {
|
|
777
|
+
return platform === 'codex'
|
|
778
|
+
? {
|
|
779
|
+
research: path.join(project, '.agents/skills/research/SKILL.md'),
|
|
780
|
+
agent: path.join(project, '.codex/agents/researcher.toml'),
|
|
781
|
+
loop: path.join(project, '.agents/skills/loop/SKILL.md'),
|
|
782
|
+
protocol: path.join(project, '.agents/skills/loop/references/bounded-loop-protocol.md'),
|
|
783
|
+
metric: path.join(project, '.agents/skills/loop/references/metric-and-guard-contract.md'),
|
|
784
|
+
workflow: path.join(project, '.codex/rules/skill-workflow-routing.md'),
|
|
785
|
+
domain: path.join(project, '.codex/rules/skill-domain-routing.md'),
|
|
786
|
+
}
|
|
787
|
+
: {
|
|
788
|
+
research: path.join(project, '.claude/skills/research/SKILL.md'),
|
|
789
|
+
agent: path.join(project, '.claude/agents/researcher.md'),
|
|
790
|
+
loop: path.join(project, '.claude/skills/loop/SKILL.md'),
|
|
791
|
+
protocol: path.join(project, '.claude/skills/loop/references/bounded-loop-protocol.md'),
|
|
792
|
+
metric: path.join(project, '.claude/skills/loop/references/metric-and-guard-contract.md'),
|
|
793
|
+
workflow: path.join(project, '.claude/rules/skill-workflow-routing.md'),
|
|
794
|
+
domain: path.join(project, '.claude/rules/skill-domain-routing.md'),
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function readPackedResearchLoop(project, platform) {
|
|
799
|
+
const paths = packedResearchLoopPaths(project, platform);
|
|
800
|
+
const values = Object.fromEntries(Object.entries(paths).map(([key, target]) => [
|
|
801
|
+
key, fs.readFileSync(target, 'utf8'),
|
|
802
|
+
]));
|
|
803
|
+
if (platform === 'codex') {
|
|
804
|
+
values.agent = parseGeneratedTomlString(values.agent, 'developer_instructions');
|
|
805
|
+
}
|
|
806
|
+
return values;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function packedResearchLoopIssues(project, platform) {
|
|
810
|
+
const values = Object.fromEntries(Object.entries(readPackedResearchLoop(project, platform)).map(
|
|
811
|
+
([key, value]) => [key, value.replace(/\s+/g, ' ').trim()]
|
|
812
|
+
));
|
|
813
|
+
const issues = new Set();
|
|
814
|
+
const publicResearch = platform === 'codex' ? 'name: cf-research' : 'name: cf:research';
|
|
815
|
+
const publicLoop = platform === 'codex' ? 'name: cf-loop' : 'name: cf:loop';
|
|
816
|
+
if (!values.research.includes(publicResearch)
|
|
817
|
+
|| !values.research.includes('Choose the smallest depth that can support the decision:')
|
|
818
|
+
|| !values.research.includes('Use delegated researchers only as optional acceleration')
|
|
819
|
+
|| !values.research.includes('research sequentially with the same evidence bar.')
|
|
820
|
+
|| !values.research.includes('Default to a concise answer in chat.')
|
|
821
|
+
|| !values.research.includes('claim, URL or repository anchor, authority, date/version, applicability to this project')) {
|
|
822
|
+
issues.add('research-adaptive-evidence');
|
|
823
|
+
}
|
|
824
|
+
if (!values.agent.includes('do not implement code, write files, mutate task state, ask the user directly, or launch another workflow.')
|
|
825
|
+
|| !values.agent.includes('owns any authorized persistence.')) {
|
|
826
|
+
issues.add('research-agent-boundary');
|
|
827
|
+
}
|
|
828
|
+
if (!values.loop.includes(publicLoop)
|
|
829
|
+
|| !values.loop.includes('Loop is explicit-only. Never auto-route ordinary implementation, debugging, or research into Loop.')
|
|
830
|
+
|| !values.loop.includes('Reject dirty in-scope state; record but never import or clean out-of-scope dirt.')
|
|
831
|
+
|| !values.loop.includes('separately approved external realpath')
|
|
832
|
+
|| !values.loop.includes('complete detached-worktree tracked/untracked manifest')
|
|
833
|
+
|| !values.loop.includes('Live-agent adherence to this written contract is `[UNPROVEN]` without a host run.')) {
|
|
834
|
+
issues.add('loop-safety');
|
|
835
|
+
}
|
|
836
|
+
if (!values.metric.includes('Guard is mandatory, fixed before baseline, and distinct from Metric.')
|
|
837
|
+
|| !values.metric.includes('`numeric_format`: exactly IEEE-754 binary64')
|
|
838
|
+
|| !values.metric.includes('nonzero exit')
|
|
839
|
+
|| !values.metric.includes('require median, noise, improvement, and required delta to remain finite.')) {
|
|
840
|
+
issues.add('metric-guard');
|
|
841
|
+
}
|
|
842
|
+
if (!values.protocol.includes('do not clean. Preserve the exact path, PID/process evidence, and ownership marker; return `BLOCKED`')
|
|
843
|
+
|| !values.protocol.includes('`base_oid`;')
|
|
844
|
+
|| !values.protocol.includes('patch byte length and lowercase SHA-256')) {
|
|
845
|
+
issues.add('failure-handoff');
|
|
846
|
+
}
|
|
847
|
+
if (!values.workflow.includes('numeric optimization capability remains explicit-only and must never be auto-routed.')
|
|
848
|
+
|| !values.domain.includes('explicit-only numeric optimization capability unless the user invokes it with its required bounded metric/guard contract.')) {
|
|
849
|
+
issues.add('explicit-routing');
|
|
850
|
+
}
|
|
851
|
+
return [...issues].sort();
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function assertPackedResearchLoopParity(project, platform) {
|
|
855
|
+
const installed = packedResearchLoopPaths(project, platform);
|
|
856
|
+
for (const [key, relative] of Object.entries(RESEARCH_LOOP_SOURCE_RELATIVES)) {
|
|
857
|
+
const sourcePath = path.join(PACKAGE_ROOT, relative);
|
|
858
|
+
const source = fs.readFileSync(sourcePath, 'utf8');
|
|
859
|
+
const expected = platform === 'codex'
|
|
860
|
+
? (key === 'agent'
|
|
861
|
+
? convertCodexAgentContent(source, path.basename(sourcePath))
|
|
862
|
+
: normalizeCodexBody(source, sourcePath))
|
|
863
|
+
: source;
|
|
864
|
+
assert.equal(fs.readFileSync(installed[key], 'utf8'), expected, `${platform} ${key} projection drifted`);
|
|
865
|
+
}
|
|
866
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(project, RUNTIMES[platform].manifest), 'utf8'));
|
|
867
|
+
const prefixes = platform === 'codex'
|
|
868
|
+
? ['.agents/skills/research/', '.agents/skills/loop/']
|
|
869
|
+
: ['skills/research/', 'skills/loop/'];
|
|
870
|
+
for (const prefix of prefixes) {
|
|
871
|
+
assert.ok(Object.keys(manifest.files).some((entry) => entry.startsWith(prefix)), `${platform} manifest missing ${prefix}`);
|
|
872
|
+
}
|
|
873
|
+
assert.deepEqual(packedResearchLoopIssues(project, platform), []);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function packedBrainstormInstalledPaths(project, platform) {
|
|
877
|
+
return platform === 'codex'
|
|
878
|
+
? {
|
|
879
|
+
skill: path.join(project, '.agents/skills/brainstorm/SKILL.md'),
|
|
880
|
+
framework: path.join(project, '.agents/skills/brainstorm/references/question-framework.md'),
|
|
881
|
+
agent: path.join(project, '.codex/agents/brainstormer.toml'),
|
|
882
|
+
}
|
|
883
|
+
: {
|
|
884
|
+
skill: path.join(project, '.claude/skills/brainstorm/SKILL.md'),
|
|
885
|
+
framework: path.join(project, '.claude/skills/brainstorm/references/question-framework.md'),
|
|
886
|
+
agent: path.join(project, '.claude/agents/brainstormer.md'),
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function packedAdaptiveBrainstormIssues(project, platform) {
|
|
891
|
+
const skill = fs.readFileSync(packedBrainstormInstalledPaths(project, platform).skill, 'utf8')
|
|
892
|
+
.replace(/\s+/g, ' ').trim();
|
|
893
|
+
return ADAPTIVE_BRAINSTORM_INSTALLED_RULES
|
|
894
|
+
.filter(({ mutations }) => mutations.some(({ clause }) => !skill.includes(clause)))
|
|
895
|
+
.map(({ group }) => group)
|
|
896
|
+
.sort();
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function assertPackedAdaptiveBrainstormMutations(project, platform, canonicalBytes) {
|
|
900
|
+
const installedScope = platform === 'codex'
|
|
901
|
+
? path.join(project, '.agents/skills/brainstorm')
|
|
902
|
+
: path.join(project, '.claude/skills/brainstorm');
|
|
903
|
+
const target = packedBrainstormInstalledPaths(project, platform).skill;
|
|
904
|
+
assertContainedRealpath(project, installedScope, `${platform} Brainstorm installed scope`);
|
|
905
|
+
const targetLstat = fs.lstatSync(target);
|
|
906
|
+
assert.equal(targetLstat.isSymbolicLink(), false, `${platform} mutation target must not be a symlink`);
|
|
907
|
+
assert.equal(targetLstat.isFile(), true, `${platform} mutation target must be a regular file`);
|
|
908
|
+
const canonicalTarget = assertContainedRealpath(installedScope, target, `${platform} Brainstorm mutation target`);
|
|
909
|
+
assert.equal(
|
|
910
|
+
canonicalTarget,
|
|
911
|
+
assertContainedRealpath(project, target, `${platform} Brainstorm project mutation target`),
|
|
912
|
+
`${platform} scope and project containment must resolve to the same target`
|
|
913
|
+
);
|
|
914
|
+
const targetStat = fs.statSync(canonicalTarget);
|
|
915
|
+
assert.equal(targetStat.nlink, 1, `${platform} mutation target must have exactly one hard link`);
|
|
916
|
+
for (const sourcePath of canonicalBytes.keys()) {
|
|
917
|
+
const canonicalSource = fs.realpathSync(sourcePath);
|
|
918
|
+
const sourceStat = fs.statSync(canonicalSource);
|
|
919
|
+
assert.notEqual(canonicalTarget, canonicalSource, `${platform} mutation target must not resolve to canonical source`);
|
|
920
|
+
assert.notDeepEqual(
|
|
921
|
+
[targetStat.dev, targetStat.ino],
|
|
922
|
+
[sourceStat.dev, sourceStat.ino],
|
|
923
|
+
`${platform} mutation target must not share a canonical source inode`
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
const original = fs.readFileSync(target);
|
|
927
|
+
const exercised = new Set();
|
|
928
|
+
assert.deepEqual(packedAdaptiveBrainstormIssues(project, platform), []);
|
|
929
|
+
for (const rule of ADAPTIVE_BRAINSTORM_INSTALLED_RULES) {
|
|
930
|
+
assert.ok(rule.mutations.length > 0, `${platform}/${rule.group} must contain at least one mutation`);
|
|
931
|
+
let completedMutations = 0;
|
|
932
|
+
for (const mutation of rule.mutations) {
|
|
933
|
+
const content = original.toString('utf8');
|
|
934
|
+
const anchor = content.indexOf(mutation.from);
|
|
935
|
+
assert.ok(anchor >= 0, `${platform}/${rule.group} anchor must exist`);
|
|
936
|
+
assert.equal(content.indexOf(mutation.from, anchor + mutation.from.length), -1, `${platform}/${rule.group} anchor must be unique`);
|
|
937
|
+
try {
|
|
938
|
+
fs.writeFileSync(target, `${content.slice(0, anchor)}${mutation.to}${content.slice(anchor + mutation.from.length)}`);
|
|
939
|
+
assert.deepEqual(packedAdaptiveBrainstormIssues(project, platform), [rule.group]);
|
|
940
|
+
for (const [sourcePath, expected] of canonicalBytes) assert.deepEqual(fs.readFileSync(sourcePath), expected);
|
|
941
|
+
} finally {
|
|
942
|
+
fs.writeFileSync(target, original);
|
|
943
|
+
}
|
|
944
|
+
assert.deepEqual(fs.readFileSync(target), original, `${platform}/${rule.group} installed bytes restored`);
|
|
945
|
+
completedMutations += 1;
|
|
946
|
+
}
|
|
947
|
+
assert.ok(completedMutations > 0, `${platform}/${rule.group} must execute at least one mutation`);
|
|
948
|
+
exercised.add(`${platform}:${rule.group}`);
|
|
949
|
+
}
|
|
950
|
+
return exercised;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function sha256(content) {
|
|
954
|
+
return createHash('sha256').update(content).digest('hex');
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function assertContainedRealpath(root, target, label) {
|
|
958
|
+
const canonicalRoot = fs.realpathSync(root);
|
|
959
|
+
const canonicalTarget = fs.realpathSync(target);
|
|
960
|
+
const relative = path.relative(canonicalRoot, canonicalTarget);
|
|
961
|
+
assert.ok(
|
|
962
|
+
relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative),
|
|
963
|
+
`${label} must stay inside ${canonicalRoot}`
|
|
964
|
+
);
|
|
965
|
+
return canonicalTarget;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function installedMutationContext(tempRoot, project, platform, packageRoot = PACKAGE_ROOT) {
|
|
969
|
+
const runtime = RUNTIMES[platform];
|
|
970
|
+
assert.ok(runtime, `unknown platform: ${platform}`);
|
|
971
|
+
const canonicalTempRoot = assertContainedRealpath(os.tmpdir(), tempRoot, `${platform} mkdtemp root`);
|
|
972
|
+
assert.match(path.basename(canonicalTempRoot), /^cafekit-package-matrix-/, `${platform} mutation root must be the verified matrix mkdtemp`);
|
|
973
|
+
const canonicalProject = assertContainedRealpath(canonicalTempRoot, project, `${platform} packed project`);
|
|
974
|
+
const installedRelatives = {
|
|
975
|
+
skill: path.join(runtime.specs, 'SKILL.md'),
|
|
976
|
+
specMaker: runtime.specMaker,
|
|
977
|
+
templates: 'references/templates.md',
|
|
978
|
+
review: 'references/review.md',
|
|
979
|
+
};
|
|
980
|
+
installedRelatives.templates = path.join(runtime.specs, installedRelatives.templates);
|
|
981
|
+
installedRelatives.review = path.join(runtime.specs, installedRelatives.review);
|
|
982
|
+
const installedPaths = Object.fromEntries(
|
|
983
|
+
Object.entries(installedRelatives).map(([source, relative]) => [source, path.join(canonicalProject, relative)])
|
|
984
|
+
);
|
|
985
|
+
const sourcePaths = Object.fromEntries(
|
|
986
|
+
Object.entries(ADAPTIVE_SOURCE_RELATIVES)
|
|
987
|
+
.map(([source, relative]) => [source, path.join(packageRoot, relative)])
|
|
988
|
+
);
|
|
989
|
+
for (const source of Object.keys(sourcePaths)) sourcePaths[source] = fs.realpathSync(sourcePaths[source]);
|
|
990
|
+
const sourceHashes = Object.fromEntries(
|
|
991
|
+
Object.entries(sourcePaths).map(([source, sourcePath]) => [source, sha256(fs.readFileSync(sourcePath))])
|
|
992
|
+
);
|
|
993
|
+
const assertSourceHashes = (stage) => {
|
|
994
|
+
for (const source of Object.keys(sourcePaths)) {
|
|
995
|
+
assert.equal(
|
|
996
|
+
sha256(fs.readFileSync(sourcePaths[source])),
|
|
997
|
+
sourceHashes[source],
|
|
998
|
+
`${platform} PACKAGE_ROOT ${source} SHA changed ${stage}`
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
const assertSafeInstalledTarget = (source, stage) => {
|
|
1003
|
+
assert.ok(Object.hasOwn(installedPaths, source), `${platform} unknown installed mutation source: ${source}`);
|
|
1004
|
+
assertSourceHashes(`before ${stage}`);
|
|
1005
|
+
|
|
1006
|
+
const currentTempRoot = assertContainedRealpath(os.tmpdir(), tempRoot, `${platform} ${stage} mkdtemp root`);
|
|
1007
|
+
assert.equal(currentTempRoot, canonicalTempRoot, `${platform} ${stage} mutation root changed`);
|
|
1008
|
+
const currentProject = assertContainedRealpath(currentTempRoot, project, `${platform} ${stage} packed project`);
|
|
1009
|
+
assert.equal(currentProject, canonicalProject, `${platform} ${stage} packed project changed`);
|
|
1010
|
+
|
|
1011
|
+
const target = installedPaths[source];
|
|
1012
|
+
const targetMetadata = fs.lstatSync(target);
|
|
1013
|
+
assert.equal(
|
|
1014
|
+
targetMetadata.isSymbolicLink(),
|
|
1015
|
+
false,
|
|
1016
|
+
`${platform} ${stage} target must be a regular non-symlink file`
|
|
1017
|
+
);
|
|
1018
|
+
assert.equal(targetMetadata.isFile(), true, `${platform} ${stage} target must be a regular non-symlink file`);
|
|
1019
|
+
const canonicalTarget = assertContainedRealpath(
|
|
1020
|
+
currentProject,
|
|
1021
|
+
target,
|
|
1022
|
+
`${platform} ${stage} installed ${source} mutation target`
|
|
1023
|
+
);
|
|
1024
|
+
assert.equal(
|
|
1025
|
+
canonicalTarget,
|
|
1026
|
+
path.join(currentProject, installedRelatives[source]),
|
|
1027
|
+
`${platform} ${stage} target must remain at its exact native installed path`
|
|
1028
|
+
);
|
|
1029
|
+
assert.notEqual(canonicalTarget, sourcePaths[source], `${platform} ${stage} target must not be package source`);
|
|
1030
|
+
|
|
1031
|
+
const installedIdentity = fs.statSync(target);
|
|
1032
|
+
for (const [protectedSource, protectedSourcePath] of Object.entries(sourcePaths)) {
|
|
1033
|
+
const sourceIdentity = fs.statSync(protectedSourcePath);
|
|
1034
|
+
assert.ok(
|
|
1035
|
+
installedIdentity.dev !== sourceIdentity.dev || installedIdentity.ino !== sourceIdentity.ino,
|
|
1036
|
+
`${platform} ${stage} target must not share (dev, ino) with any package source: ${protectedSource}`
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
assert.equal(
|
|
1040
|
+
installedIdentity.nlink,
|
|
1041
|
+
1,
|
|
1042
|
+
`${platform} ${stage} target must have exactly one hard link`
|
|
1043
|
+
);
|
|
1044
|
+
return target;
|
|
1045
|
+
};
|
|
1046
|
+
const writeInstalled = (source, content, stage) => {
|
|
1047
|
+
const target = assertSafeInstalledTarget(source, stage);
|
|
1048
|
+
fs.writeFileSync(target, content);
|
|
1049
|
+
assertSourceHashes(`immediately after ${stage}`);
|
|
1050
|
+
};
|
|
1051
|
+
for (const source of Object.keys(installedPaths)) {
|
|
1052
|
+
assertSafeInstalledTarget(source, `${source} context creation`);
|
|
1053
|
+
}
|
|
1054
|
+
return { installedPaths, sourcePaths, assertSafeInstalledTarget, writeInstalled, assertSourceHashes };
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
test('installed mutation guard rejects post-context symlink and hardlink before source bytes change', () => {
|
|
1058
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-package-matrix-'));
|
|
1059
|
+
const trackedSources = Object.fromEntries(
|
|
1060
|
+
Object.entries(ADAPTIVE_SOURCE_RELATIVES)
|
|
1061
|
+
.map(([source, relative]) => [source, path.join(PACKAGE_ROOT, relative)])
|
|
1062
|
+
);
|
|
1063
|
+
const trackedHashes = Object.fromEntries(
|
|
1064
|
+
Object.entries(trackedSources).map(([source, sourcePath]) => [source, sha256(fs.readFileSync(sourcePath))])
|
|
1065
|
+
);
|
|
1066
|
+
try {
|
|
1067
|
+
for (const topology of ['symlink', 'hardlink']) {
|
|
1068
|
+
for (const source of Object.keys(ADAPTIVE_SOURCE_RELATIVES)) {
|
|
1069
|
+
const packageRoot = path.join(root, `${topology}-${source}-package-source`);
|
|
1070
|
+
const project = path.join(root, `${topology}-${source}-project`);
|
|
1071
|
+
for (const [fixtureSource, relative] of Object.entries(ADAPTIVE_SOURCE_RELATIVES)) {
|
|
1072
|
+
const sourcePath = path.join(packageRoot, relative);
|
|
1073
|
+
fs.mkdirSync(path.dirname(sourcePath), { recursive: true });
|
|
1074
|
+
fs.writeFileSync(sourcePath, `${topology} disposable source ${fixtureSource}\n`);
|
|
1075
|
+
}
|
|
1076
|
+
for (const [fixtureSource, installedPath] of Object.entries(adaptiveInstalledPaths(project, 'claude'))) {
|
|
1077
|
+
fs.mkdirSync(path.dirname(installedPath), { recursive: true });
|
|
1078
|
+
fs.writeFileSync(installedPath, `${topology} disposable installed ${fixtureSource}\n`);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const context = installedMutationContext(root, project, 'claude', packageRoot);
|
|
1082
|
+
const sourcePath = context.sourcePaths[source];
|
|
1083
|
+
const target = context.installedPaths[source];
|
|
1084
|
+
const sourceBytes = fs.readFileSync(sourcePath);
|
|
1085
|
+
const sourceHash = sha256(sourceBytes);
|
|
1086
|
+
fs.unlinkSync(target);
|
|
1087
|
+
if (topology === 'symlink') {
|
|
1088
|
+
fs.symlinkSync(sourcePath, target);
|
|
1089
|
+
assert.equal(fs.lstatSync(target).isSymbolicLink(), true, 'symlink control must change target topology');
|
|
1090
|
+
} else {
|
|
1091
|
+
fs.linkSync(sourcePath, target);
|
|
1092
|
+
const sourceIdentity = fs.statSync(sourcePath);
|
|
1093
|
+
const installedIdentity = fs.statSync(target);
|
|
1094
|
+
assert.deepEqual(
|
|
1095
|
+
[installedIdentity.dev, installedIdentity.ino],
|
|
1096
|
+
[sourceIdentity.dev, sourceIdentity.ino],
|
|
1097
|
+
'hardlink control must share source identity'
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
assert.throws(
|
|
1102
|
+
() => context.writeInstalled(source, 'forbidden installed mutation\n', `${topology} regression`),
|
|
1103
|
+
topology === 'symlink' ? /regular non-symlink file/ : /must not share \(dev, ino\)/
|
|
1104
|
+
);
|
|
1105
|
+
assert.deepEqual(fs.readFileSync(sourcePath), sourceBytes, `${topology} rejection must preserve source bytes`);
|
|
1106
|
+
assert.equal(sha256(fs.readFileSync(sourcePath)), sourceHash, `${topology} rejection must preserve source SHA`);
|
|
1107
|
+
context.assertSourceHashes(`after rejected ${topology} write`);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const crossSourcePackageRoot = path.join(root, 'cross-source-package-source');
|
|
1112
|
+
const crossSourceProject = path.join(root, 'cross-source-project');
|
|
1113
|
+
for (const [source, relative] of Object.entries(ADAPTIVE_SOURCE_RELATIVES)) {
|
|
1114
|
+
const sourcePath = path.join(crossSourcePackageRoot, relative);
|
|
1115
|
+
fs.mkdirSync(path.dirname(sourcePath), { recursive: true });
|
|
1116
|
+
fs.writeFileSync(sourcePath, `cross-source disposable source ${source}\n`);
|
|
1117
|
+
}
|
|
1118
|
+
for (const [source, installedPath] of Object.entries(adaptiveInstalledPaths(crossSourceProject, 'claude'))) {
|
|
1119
|
+
fs.mkdirSync(path.dirname(installedPath), { recursive: true });
|
|
1120
|
+
fs.writeFileSync(installedPath, `cross-source disposable installed ${source}\n`);
|
|
1121
|
+
}
|
|
1122
|
+
const crossSourceContext = installedMutationContext(
|
|
1123
|
+
root,
|
|
1124
|
+
crossSourceProject,
|
|
1125
|
+
'claude',
|
|
1126
|
+
crossSourcePackageRoot
|
|
1127
|
+
);
|
|
1128
|
+
const targetSource = 'skill';
|
|
1129
|
+
const linkedSource = 'specMaker';
|
|
1130
|
+
assert.notEqual(targetSource, linkedSource, 'cross-source control requires A != B');
|
|
1131
|
+
const crossSourceTarget = crossSourceContext.installedPaths[targetSource];
|
|
1132
|
+
const crossSourceTargetBytes = fs.readFileSync(crossSourceTarget);
|
|
1133
|
+
const crossSourceState = Object.fromEntries(
|
|
1134
|
+
Object.entries(crossSourceContext.sourcePaths).map(([source, sourcePath]) => {
|
|
1135
|
+
const bytes = fs.readFileSync(sourcePath);
|
|
1136
|
+
return [source, { bytes, sha: sha256(bytes) }];
|
|
1137
|
+
})
|
|
1138
|
+
);
|
|
1139
|
+
fs.unlinkSync(crossSourceTarget);
|
|
1140
|
+
fs.linkSync(crossSourceContext.sourcePaths[linkedSource], crossSourceTarget);
|
|
1141
|
+
try {
|
|
1142
|
+
assert.throws(
|
|
1143
|
+
() => crossSourceContext.writeInstalled(
|
|
1144
|
+
targetSource,
|
|
1145
|
+
'forbidden packed cross-source mutation\n',
|
|
1146
|
+
'cross-source hardlink regression'
|
|
1147
|
+
),
|
|
1148
|
+
/must not share \(dev, ino\) with any package source: specMaker/
|
|
1149
|
+
);
|
|
1150
|
+
} finally {
|
|
1151
|
+
fs.unlinkSync(crossSourceTarget);
|
|
1152
|
+
fs.writeFileSync(crossSourceTarget, crossSourceTargetBytes);
|
|
1153
|
+
fs.writeFileSync(
|
|
1154
|
+
crossSourceContext.sourcePaths[linkedSource],
|
|
1155
|
+
crossSourceState[linkedSource].bytes
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
assert.deepEqual(
|
|
1159
|
+
fs.readFileSync(crossSourceTarget),
|
|
1160
|
+
crossSourceTargetBytes,
|
|
1161
|
+
'cross-source control must restore exact installed bytes'
|
|
1162
|
+
);
|
|
1163
|
+
for (const [source, sourcePath] of Object.entries(crossSourceContext.sourcePaths)) {
|
|
1164
|
+
const actual = fs.readFileSync(sourcePath);
|
|
1165
|
+
assert.deepEqual(actual, crossSourceState[source].bytes, `cross-source rejection must preserve ${source} bytes`);
|
|
1166
|
+
assert.equal(sha256(actual), crossSourceState[source].sha, `cross-source rejection must preserve ${source} SHA`);
|
|
1167
|
+
}
|
|
1168
|
+
crossSourceContext.assertSourceHashes('after rejected cross-source hardlink write');
|
|
1169
|
+
|
|
1170
|
+
for (const [source, sourcePath] of Object.entries(trackedSources)) {
|
|
1171
|
+
assert.equal(
|
|
1172
|
+
sha256(fs.readFileSync(sourcePath)),
|
|
1173
|
+
trackedHashes[source],
|
|
1174
|
+
`tracked PACKAGE_ROOT ${source} SHA must remain unchanged`
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
} finally {
|
|
1178
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
function assertInstalledReadinessMutationsDetected(tempRoot, project, platform) {
|
|
1183
|
+
const context = installedMutationContext(tempRoot, project, platform);
|
|
1184
|
+
const baselineBytes = Object.fromEntries(
|
|
1185
|
+
Object.entries(context.installedPaths).map(([source, target]) => [source, fs.readFileSync(target)])
|
|
1186
|
+
);
|
|
1187
|
+
const baseline = Object.fromEntries(
|
|
1188
|
+
Object.entries(baselineBytes).map(([source, bytes]) => [source, bytes.toString('utf8')])
|
|
1189
|
+
);
|
|
1190
|
+
const boundaryMutation = (name, boundary, replacements) => {
|
|
1191
|
+
const row = IMPLEMENTATION_READINESS_BOUNDARY_ROWS.find(([label]) => label === boundary);
|
|
1192
|
+
assert.ok(row, `${name} references unknown boundary ${boundary}`);
|
|
1193
|
+
let weakened = row[1];
|
|
1194
|
+
for (const [from, to] of replacements) {
|
|
1195
|
+
const next = weakened.replace(from, to);
|
|
1196
|
+
assert.notEqual(next, weakened, `${name} weakening anchor must exist in ${boundary}`);
|
|
1197
|
+
weakened = next;
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
name, issue: 'boundary-contract', source: 'templates',
|
|
1201
|
+
from: `| ${row[0]} | ${row[1]} |`,
|
|
1202
|
+
to: `| ${row[0]} | ${weakened} |`,
|
|
1203
|
+
};
|
|
1204
|
+
};
|
|
1205
|
+
const mutations = [
|
|
1206
|
+
{
|
|
1207
|
+
name: 'no-invention-blocking', issue: 'no-invention', source: 'templates',
|
|
1208
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.noInvention,
|
|
1209
|
+
to: 'Before implementation handoff, note ambiguous choices without blocking handoff.',
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
name: 'no-invention-contradictory-override', issue: 'no-invention', source: 'templates',
|
|
1213
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.noInvention,
|
|
1214
|
+
to: `${IMPLEMENTATION_READINESS_CLAUSES.noInvention}\n\nException: implementation handoff may proceed with an unresolved material choice.`,
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
name: 'material-boundary-definition', issue: 'boundary-contract', source: 'templates',
|
|
1218
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.materialDefinition,
|
|
1219
|
+
to: 'A boundary is material when it seems relevant to the task.',
|
|
1220
|
+
},
|
|
1221
|
+
{
|
|
1222
|
+
name: 'exact-boundary-choices', issue: 'boundary-contract', source: 'templates',
|
|
1223
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.exactBoundaryChoices,
|
|
1224
|
+
to: 'For every required row, describe the listed choices generally.',
|
|
1225
|
+
},
|
|
1226
|
+
boundaryMutation('interaction-accessibility', 'Interaction/UI', [
|
|
1227
|
+
['input/focus/keyboard; ', ''],
|
|
1228
|
+
['accessibility; ', ''],
|
|
1229
|
+
]),
|
|
1230
|
+
boundaryMutation('api-success-and-error-semantics', 'API/CLI', [
|
|
1231
|
+
['success output; ', ''],
|
|
1232
|
+
['error/status/exit; ', ''],
|
|
1233
|
+
]),
|
|
1234
|
+
boundaryMutation('schema-shape-and-unknown-fields', 'Data/schema', [
|
|
1235
|
+
['exact keys/nesting/types; ', ''],
|
|
1236
|
+
['unknown-field behavior; ', ''],
|
|
1237
|
+
]),
|
|
1238
|
+
boundaryMutation('schema-enum-and-format', 'Data/schema', [
|
|
1239
|
+
['enum/format/', ''],
|
|
1240
|
+
]),
|
|
1241
|
+
boundaryMutation('state-lock-lifecycle', 'Async/state', [
|
|
1242
|
+
['writer/lock acquire/contention/release; ', 'writer/lock; '],
|
|
1243
|
+
]),
|
|
1244
|
+
boundaryMutation('filesystem-segment-grammar', 'Filesystem/security', [
|
|
1245
|
+
['trusted/untrusted segment grammar; ', ''],
|
|
1246
|
+
]),
|
|
1247
|
+
boundaryMutation('filesystem-stale-lock-reclaim', 'Filesystem/security', [
|
|
1248
|
+
['lock/stale reclaim; ', ''],
|
|
1249
|
+
]),
|
|
1250
|
+
boundaryMutation('runtime-rollout-and-recovery', 'Runtime/deploy', [
|
|
1251
|
+
['rollout/rollback; ', ''],
|
|
1252
|
+
['operator recovery', ''],
|
|
1253
|
+
]),
|
|
1254
|
+
boundaryMutation('retention-clock-and-endpoints', 'Time/retention', [
|
|
1255
|
+
['clock source; ', ''],
|
|
1256
|
+
['unit/precision/timezone; ', ''],
|
|
1257
|
+
['endpoints and inclusion/comparator; ', ''],
|
|
1258
|
+
]),
|
|
1259
|
+
boundaryMutation('ai-model-safety-and-eval', 'AI/model', [
|
|
1260
|
+
['safety/privacy; ', ''],
|
|
1261
|
+
['eval oracle', ''],
|
|
1262
|
+
]),
|
|
1263
|
+
boundaryMutation('proof-level-partition', 'Integration/proof', [
|
|
1264
|
+
['proof level (`source`/`installed`/`live`)', 'proof level'],
|
|
1265
|
+
]),
|
|
1266
|
+
{
|
|
1267
|
+
name: 'concrete-named-probe', issue: 'proof-chain', source: 'templates',
|
|
1268
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[1],
|
|
1269
|
+
to: '- Named probe: <suite label>',
|
|
1270
|
+
},
|
|
1271
|
+
{
|
|
1272
|
+
name: 'aggregate-suite-probe-owner', issue: 'proof-chain', source: 'templates',
|
|
1273
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.namedProbeOwnership,
|
|
1274
|
+
to: 'Aggregate suites may cite only the suite label.',
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
name: 'reachability-levels', issue: 'proof-chain', source: 'templates',
|
|
1278
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[2],
|
|
1279
|
+
to: '- Reachability: <entrypoint or consumer>',
|
|
1280
|
+
},
|
|
1281
|
+
{
|
|
1282
|
+
name: 'proof-level-separation', issue: 'proof-chain', source: 'templates',
|
|
1283
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofLevelSeparation,
|
|
1284
|
+
to: 'Proof may be promoted between source, installed, and live levels.',
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
name: 'disposable-template-negative-controls', issue: 'proof-chain', source: 'templates',
|
|
1288
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.disposableTemplateControls,
|
|
1289
|
+
to: 'Run mutation or destructive negative controls against the available project copy.',
|
|
1290
|
+
},
|
|
1291
|
+
{
|
|
1292
|
+
name: 'disposable-review-negative-controls', issue: 'proof-chain', source: 'review',
|
|
1293
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.disposableReviewControls,
|
|
1294
|
+
to: 'Run mutation or destructive negative controls against the available project copy.',
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
name: 'required-proof-level-mapping', issue: 'proof-chain', source: 'templates',
|
|
1298
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofLevelMapping,
|
|
1299
|
+
to: 'Map one required proof level to one probe; other levels may remain implicit.',
|
|
1300
|
+
},
|
|
1301
|
+
{
|
|
1302
|
+
name: 'artifact-path-and-digest-or-ephemeral', issue: 'proof-chain', source: 'templates',
|
|
1303
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[5],
|
|
1304
|
+
to: '- Artifacts: <required artifact path, or none>',
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
name: 'proof-counterexample', issue: 'proof-chain', source: 'templates',
|
|
1308
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[4],
|
|
1309
|
+
to: '- Counterexample: <example>',
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
name: 'repair-and-proof-columns', issue: 'repair-closure', source: 'review',
|
|
1313
|
+
from: '| ID | Decision | Original counterexample | Repaired at | Proved at | Replay | Closure |',
|
|
1314
|
+
to: '| ID | Decision | Original counterexample | Repaired at | Evidence | Replay | Closure |',
|
|
1315
|
+
},
|
|
1316
|
+
{
|
|
1317
|
+
name: 'fresh-original-counterexample-replay', issue: 'repair-closure', source: 'review',
|
|
1318
|
+
from: 'After applying an accepted C2 finding, a fresh-context closure pass records and\nfreshly replays its original counterexample after the repair under this exact review-log header:',
|
|
1319
|
+
to: 'After applying an accepted C2 finding, record the repair under this review-log header:',
|
|
1320
|
+
},
|
|
1321
|
+
{
|
|
1322
|
+
name: 'distinct-repair-and-proof-evidence', issue: 'repair-closure', source: 'review',
|
|
1323
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.distinctRepairProof,
|
|
1324
|
+
to: '`Repaired at` and `Proved at` may cite the same repair edit.',
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
name: 'unknown-blocks-handoff', issue: 'repair-closure', source: 'review',
|
|
1328
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.unknownBlocks,
|
|
1329
|
+
to: '`PASS` closes it; `FAIL` and `UNKNOWN` may continue to implementation handoff.',
|
|
1330
|
+
},
|
|
1331
|
+
{
|
|
1332
|
+
name: 'crash-versus-catchable-failure', issue: 'failure-semantics', source: 'templates',
|
|
1333
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.failureSemantics,
|
|
1334
|
+
to: 'Crash and catchable failure both mean an error occurred.',
|
|
1335
|
+
},
|
|
1336
|
+
{
|
|
1337
|
+
name: 'privacy-identifier-surface', issue: 'privacy-identifiers', source: 'review',
|
|
1338
|
+
from: IMPLEMENTATION_READINESS_CLAUSES.privacyIdentifiers,
|
|
1339
|
+
to: 'Any privacy/security claim names the sensitive data at risk.',
|
|
1340
|
+
},
|
|
1341
|
+
];
|
|
1342
|
+
assert.equal(mutations.length, 30, `${platform} readiness mutation inventory drifted`);
|
|
1343
|
+
assertInstalledSpecsReadiness(project, platform);
|
|
1344
|
+
for (const mutation of mutations) {
|
|
1345
|
+
const anchorIndex = baseline[mutation.source].indexOf(mutation.from);
|
|
1346
|
+
assert.notEqual(anchorIndex, -1, `${platform} ${mutation.name} mutation anchor must exist`);
|
|
1347
|
+
assert.equal(
|
|
1348
|
+
baseline[mutation.source].indexOf(mutation.from, anchorIndex + mutation.from.length),
|
|
1349
|
+
-1,
|
|
1350
|
+
`${platform} ${mutation.name} mutation anchor must be unique`
|
|
1351
|
+
);
|
|
1352
|
+
const weakened = `${baseline[mutation.source].slice(0, anchorIndex)}${mutation.to}${baseline[mutation.source].slice(anchorIndex + mutation.from.length)}`;
|
|
1353
|
+
try {
|
|
1354
|
+
context.writeInstalled(mutation.source, weakened, `${mutation.name} mutation`);
|
|
1355
|
+
assertInstalledSpecsReadiness(project, platform, [mutation.issue]);
|
|
1356
|
+
} finally {
|
|
1357
|
+
context.writeInstalled(mutation.source, baselineBytes[mutation.source], `${mutation.name} restoration`);
|
|
1358
|
+
}
|
|
1359
|
+
assert.deepEqual(
|
|
1360
|
+
fs.readFileSync(context.installedPaths[mutation.source]),
|
|
1361
|
+
baselineBytes[mutation.source],
|
|
1362
|
+
`${platform} ${mutation.name} must restore exact installed bytes`
|
|
1363
|
+
);
|
|
1364
|
+
assertInstalledSpecsReadiness(project, platform);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
const integrationRow = IMPLEMENTATION_READINESS_BOUNDARY_ROWS
|
|
1368
|
+
.find(([label]) => label === 'Integration/proof');
|
|
1369
|
+
const integrationLine = `| ${integrationRow.join(' | ')} |`;
|
|
1370
|
+
const openBoundary = baseline.templates.replace(
|
|
1371
|
+
integrationLine,
|
|
1372
|
+
`${integrationLine}\n| other:domain-specific | task-specific material choices and observable oracle |`
|
|
1373
|
+
);
|
|
1374
|
+
assert.notEqual(openBoundary, baseline.templates, `${platform} readiness positive control must apply`);
|
|
1375
|
+
try {
|
|
1376
|
+
context.writeInstalled('templates', openBoundary, 'readiness open-boundary positive control');
|
|
1377
|
+
assertInstalledSpecsReadiness(project, platform);
|
|
1378
|
+
} finally {
|
|
1379
|
+
context.writeInstalled('templates', baselineBytes.templates, 'readiness open-boundary restoration');
|
|
1380
|
+
}
|
|
1381
|
+
assert.deepEqual(
|
|
1382
|
+
fs.readFileSync(context.installedPaths.templates),
|
|
1383
|
+
baselineBytes.templates,
|
|
1384
|
+
`${platform} readiness positive control must restore exact installed bytes`
|
|
1385
|
+
);
|
|
1386
|
+
context.assertSourceHashes('after readiness mutations and positive control');
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function assertInstalledAdaptiveMutationsDetected(tempRoot, project, platform) {
|
|
1390
|
+
const context = installedMutationContext(tempRoot, project, platform);
|
|
1391
|
+
const baselineBytes = Object.fromEntries(
|
|
1392
|
+
Object.entries(context.installedPaths).map(([source, target]) => [source, fs.readFileSync(target)])
|
|
1393
|
+
);
|
|
1394
|
+
const baseline = installedAdaptiveCoverageSources(project, platform);
|
|
1395
|
+
const mutations = [
|
|
1396
|
+
['frontmatter-risk-bypass', 'skill', ADAPTIVE_COVERAGE_CLAUSES.frontmatterGate,
|
|
1397
|
+
'skip for any clear one-file or two-file change.', ['risk-first-routing']],
|
|
1398
|
+
['destructive-routine-direct', 'skill', ADAPTIVE_COVERAGE_CLAUSES.directGate,
|
|
1399
|
+
'Work directly when the change is routine and likely limited to one or two files.', ['risk-first-routing']],
|
|
1400
|
+
['destructive-direct-exception', 'skill', ADAPTIVE_COVERAGE_CLAUSES.directGate,
|
|
1401
|
+
`${ADAPTIVE_COVERAGE_CLAUSES.directGate} Exception: destructive one-file work labeled routine may go direct.`, ['risk-first-routing']],
|
|
1402
|
+
['user-risk-downgrade', 'skill', ADAPTIVE_COVERAGE_CLAUSES.riskFirst,
|
|
1403
|
+
`${ADAPTIVE_COVERAGE_CLAUSES.riskFirst} A user may lower critical risk to routine.`, ['risk-first-routing']],
|
|
1404
|
+
['four-subsystem-split', 'skill', ADAPTIVE_COVERAGE_CLAUSES.splitRoute,
|
|
1405
|
+
'Split four or more independent subsystems; otherwise use one Specs packet for substantial work.', ['risk-first-routing']],
|
|
1406
|
+
['forced-single-kind', 'templates', ADAPTIVE_COVERAGE_CLAUSES.openKinds,
|
|
1407
|
+
'Choose one primary change kind and ignore unfamiliar kinds or surfaces.', ['coverage-profile-shape']],
|
|
1408
|
+
['missing-profile-column', 'templates', '| ID | Outcome | Change kinds | Material surfaces | Ambiguity/action | Risk/evidence | Required proof |',
|
|
1409
|
+
'| ID | Outcome | Change kinds | Material surfaces | Risk/evidence | Required proof |', ['coverage-profile-shape']],
|
|
1410
|
+
['duplicate-profile-heading', 'templates', '## Coverage profile\n',
|
|
1411
|
+
'## Coverage profile\n\n## Coverage profile\n', ['coverage-profile-shape']],
|
|
1412
|
+
['truncated-profile-row', 'templates', `| ${ADAPTIVE_COVERAGE_PROFILE_ROW.join(' | ')} |`,
|
|
1413
|
+
'| CP-01 | <externally observable outcome> |', ['coverage-profile-shape']],
|
|
1414
|
+
['examples-promote-to-design', 'templates', `| ${ADAPTIVE_COVERAGE_AMBIGUITY_ROWS[2].join(' | ')} |`,
|
|
1415
|
+
'| `examples-needed` | add examples and promote to `design-needed` if behavior changes |', ['ambiguity-actions']],
|
|
1416
|
+
['examples-select-retention', 'templates', 'retention of 30 versus 90 days is\n`decision-needed`',
|
|
1417
|
+
'retention of 30 versus 90 days may remain\n`examples-needed`', ['ambiguity-actions']],
|
|
1418
|
+
['global-critical-ceremony', 'templates', ADAPTIVE_COVERAGE_CLAUSES.scopedUnion,
|
|
1419
|
+
'Each task copies its CP values; authoring, review, edge, and proof obligations union across every task.', ['scoped-coverage']],
|
|
1420
|
+
['scoped-union-contradiction', 'templates', ADAPTIVE_COVERAGE_CLAUSES.scopedUnion,
|
|
1421
|
+
`${ADAPTIVE_COVERAGE_CLAUSES.scopedUnion} Exception: critical proof obligations apply across every CP row.`, ['scoped-coverage']],
|
|
1422
|
+
['stale-profile-after-c2', 'templates', ADAPTIVE_COVERAGE_CLAUSES.profileRederivation,
|
|
1423
|
+
'Keep existing coverage rows after accepted plan changes.', ['profile-lifecycle']],
|
|
1424
|
+
['planned-proof-blocks-start', 'templates', ADAPTIVE_COVERAGE_CLAUSES.plannedProof,
|
|
1425
|
+
'`Required proof` is execution evidence: known but unrun proof blocks `pending`; `UNKNOWN` may proceed; missing evidence may still reach `done`/C3.', ['proof-lifecycle']],
|
|
1426
|
+
['source-promotes-live', 'templates', ADAPTIVE_COVERAGE_CLAUSES.proofSeparation,
|
|
1427
|
+
'Source proof may promote installed and live proof.', ['proof-lifecycle']],
|
|
1428
|
+
['unmapped-proof-level', 'templates', IMPLEMENTATION_READINESS_CLAUSES.proofLevelMapping,
|
|
1429
|
+
'A task may map only one required proof level to its probe.', ['proof-lifecycle']],
|
|
1430
|
+
['critical-reviewer-omitted', 'review', ADAPTIVE_COVERAGE_CLAUSES.reviewRisk,
|
|
1431
|
+
'Keep Fact Checker as the baseline and choose any remaining reviewer; critical rows need no matching risk role.', ['reviewer-routing', 'scoped-coverage']],
|
|
1432
|
+
['reviewer-lens-overflow', 'review', ADAPTIVE_COVERAGE_CLAUSES.reviewCapacity,
|
|
1433
|
+
'Each reviewer owns exactly one lens; skip excess material lenses when the fixed reviewer count is full.', ['reviewer-routing']],
|
|
1434
|
+
['spec-maker-local-taxonomy', 'specMaker', ADAPTIVE_COVERAGE_CLAUSES.specMakerAuthority,
|
|
1435
|
+
'this agent owns a separate risk and coverage taxonomy.', ['spec-maker-authority']],
|
|
1436
|
+
['spec-maker-examples-decide', 'specMaker', ADAPTIVE_COVERAGE_CLAUSES.specMakerAmbiguity,
|
|
1437
|
+
'Use examples to settle every ambiguous observable behavior.', ['spec-maker-authority']],
|
|
1438
|
+
['spec-maker-skips-brainstorm', 'specMaker', ADAPTIVE_COVERAGE_CLAUSES.specMakerRoute,
|
|
1439
|
+
'Apply the risk-first route before C1 and stop only for direct work.', ['spec-maker-authority']],
|
|
1440
|
+
['static-proves-live', 'skill', ADAPTIVE_COVERAGE_CLAUSES.liveLimit,
|
|
1441
|
+
'Source/static checks prove live-model adherence.', ['proof-lifecycle']],
|
|
1442
|
+
];
|
|
1443
|
+
assert.equal(mutations.length, 23, `${platform} adaptive mutation inventory drifted`);
|
|
1444
|
+
assertInstalledAdaptiveCoverage(project, platform);
|
|
1445
|
+
|
|
1446
|
+
const renderMutation = (source, value) => {
|
|
1447
|
+
if (platform === 'codex' && source === 'specMaker') {
|
|
1448
|
+
return Buffer.from(rewriteGeneratedTomlString(
|
|
1449
|
+
baselineBytes.specMaker.toString('utf8'),
|
|
1450
|
+
'developer_instructions',
|
|
1451
|
+
value
|
|
1452
|
+
));
|
|
1453
|
+
}
|
|
1454
|
+
return Buffer.from(value);
|
|
1455
|
+
};
|
|
1456
|
+
for (const [name, source, from, to, expected] of mutations) {
|
|
1457
|
+
const anchorIndex = baseline[source].indexOf(from);
|
|
1458
|
+
assert.notEqual(anchorIndex, -1, `${platform} ${name} mutation anchor must exist`);
|
|
1459
|
+
assert.equal(
|
|
1460
|
+
baseline[source].indexOf(from, anchorIndex + from.length),
|
|
1461
|
+
-1,
|
|
1462
|
+
`${platform} ${name} mutation anchor must be unique`
|
|
1463
|
+
);
|
|
1464
|
+
const weakened = `${baseline[source].slice(0, anchorIndex)}${to}${baseline[source].slice(anchorIndex + from.length)}`;
|
|
1465
|
+
const weakenedBytes = renderMutation(source, weakened);
|
|
1466
|
+
try {
|
|
1467
|
+
context.writeInstalled(source, weakenedBytes, `${name} adaptive mutation`);
|
|
1468
|
+
if (platform === 'codex' && source === 'specMaker') {
|
|
1469
|
+
assert.equal(
|
|
1470
|
+
parseGeneratedTomlString(fs.readFileSync(context.installedPaths.specMaker, 'utf8'), 'developer_instructions'),
|
|
1471
|
+
weakened,
|
|
1472
|
+
`${platform} ${name} must rewrite only parsed developer_instructions`
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
assertInstalledAdaptiveCoverage(project, platform, [...expected].sort());
|
|
1476
|
+
} finally {
|
|
1477
|
+
context.writeInstalled(source, baselineBytes[source], `${name} adaptive restoration`);
|
|
1478
|
+
}
|
|
1479
|
+
assert.deepEqual(
|
|
1480
|
+
fs.readFileSync(context.installedPaths[source]),
|
|
1481
|
+
baselineBytes[source],
|
|
1482
|
+
`${platform} ${name} must restore exact installed bytes`
|
|
1483
|
+
);
|
|
1484
|
+
assertInstalledAdaptiveCoverage(project, platform);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
const integrationRow = IMPLEMENTATION_READINESS_BOUNDARY_ROWS
|
|
1488
|
+
.find(([label]) => label === 'Integration/proof');
|
|
1489
|
+
const integrationLine = `| ${integrationRow.join(' | ')} |`;
|
|
1490
|
+
const validVariants = [
|
|
1491
|
+
['open material surface', baseline.templates.replace(
|
|
1492
|
+
integrationLine,
|
|
1493
|
+
`${integrationLine}\n| other:domain-specific | task-specific material choices and observable oracle |`
|
|
1494
|
+
)],
|
|
1495
|
+
['task CP reference requirement', baseline.templates.replace(
|
|
1496
|
+
ADAPTIVE_COVERAGE_CLAUSES.scopedUnion,
|
|
1497
|
+
`${ADAPTIVE_COVERAGE_CLAUSES.scopedUnion} Require every task to reference a CP row.`
|
|
1498
|
+
)],
|
|
1499
|
+
];
|
|
1500
|
+
for (const [name, templates] of validVariants) {
|
|
1501
|
+
assert.notEqual(templates, baseline.templates, `${platform} ${name} positive control must apply`);
|
|
1502
|
+
try {
|
|
1503
|
+
context.writeInstalled('templates', Buffer.from(templates), `${name} adaptive positive control`);
|
|
1504
|
+
assertInstalledAdaptiveCoverage(project, platform);
|
|
1505
|
+
} finally {
|
|
1506
|
+
context.writeInstalled('templates', baselineBytes.templates, `${name} adaptive restoration`);
|
|
1507
|
+
}
|
|
1508
|
+
assert.deepEqual(
|
|
1509
|
+
fs.readFileSync(context.installedPaths.templates),
|
|
1510
|
+
baselineBytes.templates,
|
|
1511
|
+
`${platform} ${name} must restore exact installed bytes`
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
assertInstalledSpecsReadiness(project, platform);
|
|
1515
|
+
context.assertSourceHashes('after adaptive mutations and positive controls');
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
function assertPristineUpgradeAndUserPreservation(installer, tempRoot, project, platform) {
|
|
1519
|
+
const runtime = RUNTIMES[platform];
|
|
1520
|
+
const context = installedMutationContext(tempRoot, project, platform);
|
|
1521
|
+
const templatesPath = context.installedPaths.templates;
|
|
1522
|
+
const manifestPath = path.join(project, runtime.manifest);
|
|
1523
|
+
const metadataPath = path.join(project, runtime.root, 'cafekit.json');
|
|
1524
|
+
const baseline = fs.readFileSync(templatesPath, 'utf8');
|
|
1525
|
+
const weakened = baseline.replace(NO_INVENTION_ANCHOR, '');
|
|
1526
|
+
assert.notEqual(weakened, baseline, `${platform} old fixture mutation must apply`);
|
|
1527
|
+
try {
|
|
1528
|
+
context.writeInstalled('templates', weakened, 'pristine-upgrade mutation');
|
|
1529
|
+
const oldManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
1530
|
+
assert.ok(oldManifest.files[runtime.templatesManifestPath], `${platform} templates ownership record missing`);
|
|
1531
|
+
oldManifest.files[runtime.templatesManifestPath] = {
|
|
1532
|
+
...oldManifest.files[runtime.templatesManifestPath], sha256: sha256(weakened), version: OLD_FIXTURE_VERSION
|
|
1533
|
+
};
|
|
1534
|
+
writeJson(manifestPath, oldManifest);
|
|
1535
|
+
const oldMetadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
1536
|
+
oldMetadata.version = OLD_FIXTURE_VERSION;
|
|
1537
|
+
writeJson(metadataPath, oldMetadata);
|
|
1538
|
+
|
|
1539
|
+
runInstaller(installer, project, [platform], null);
|
|
1540
|
+
context.assertSourceHashes('immediately after pristine-upgrade installer');
|
|
1541
|
+
context.assertSafeInstalledTarget('templates', 'after pristine-upgrade installer');
|
|
1542
|
+
assert.equal(fs.readFileSync(templatesPath, 'utf8'), baseline, `${platform} pristine old gate must upgrade`);
|
|
1543
|
+
assert.equal(JSON.parse(fs.readFileSync(metadataPath, 'utf8')).version, PACKAGE_VERSION);
|
|
1544
|
+
const upgradedManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
1545
|
+
assert.equal(upgradedManifest.files[runtime.templatesManifestPath].sha256, sha256(baseline));
|
|
1546
|
+
assert.equal(upgradedManifest.files[runtime.templatesManifestPath].version, PACKAGE_VERSION);
|
|
1547
|
+
assertInstalledSpecsReadiness(project, platform);
|
|
1548
|
+
|
|
1549
|
+
context.writeInstalled('templates', weakened, 'user-preservation mutation after installer');
|
|
1550
|
+
const manifestBeforePreservedRerun = fs.readFileSync(manifestPath, 'utf8');
|
|
1551
|
+
const preserved = runInstaller(installer, project, [platform], null);
|
|
1552
|
+
context.assertSourceHashes('immediately after user-preservation installer');
|
|
1553
|
+
context.assertSafeInstalledTarget('templates', 'after user-preservation installer');
|
|
1554
|
+
assert.equal(fs.readFileSync(templatesPath, 'utf8'), weakened, `${platform} user edit must be preserved`);
|
|
1555
|
+
assert.equal(fs.readFileSync(manifestPath, 'utf8'), manifestBeforePreservedRerun, `${platform} user-modified manifest must stay unchanged`);
|
|
1556
|
+
assert.match(`${preserved.stdout}\n${preserved.stderr}`, /preserved \(user-modified\): Skill: specs/);
|
|
1557
|
+
assertInstalledSpecsReadiness(project, platform, ['no-invention']);
|
|
1558
|
+
} finally {
|
|
1559
|
+
context.writeInstalled('templates', baseline, 'upgrade and user-preservation restoration');
|
|
1560
|
+
}
|
|
1561
|
+
assertInstalledSpecsReadiness(project, platform);
|
|
134
1562
|
}
|
|
135
1563
|
|
|
136
1564
|
function createProvenanceFixture(root) {
|
|
@@ -309,8 +1737,8 @@ function assertCombinedInstructionIsolation(root) {
|
|
|
309
1737
|
[claude, '<!-- CAFEKIT CLAUDE START -->']
|
|
310
1738
|
]) assert.equal((content.match(new RegExp(marker, 'g')) || []).length, 1);
|
|
311
1739
|
|
|
312
|
-
assert.doesNotMatch(core, /Claude|Codex|\.claude|\.codex|\/
|
|
313
|
-
assert.doesNotMatch(claudeBlock, /Codex|\.codex|\$
|
|
1740
|
+
assert.doesNotMatch(core, /Claude|Codex|\.claude|\.codex|\/cf:|\$cf-/i);
|
|
1741
|
+
assert.doesNotMatch(claudeBlock, /Codex|\.codex|\$cf-/i);
|
|
314
1742
|
assert.match(codexBlock, /native project instruction surface is root `AGENTS\.md`/);
|
|
315
1743
|
assert.match(agents, /shared-root trade-off is intentional/);
|
|
316
1744
|
// H5 ownership/ignore contract
|
|
@@ -319,7 +1747,7 @@ function assertCombinedInstructionIsolation(root) {
|
|
|
319
1747
|
assert.match(codexBlock, /owned by Codex/i);
|
|
320
1748
|
assert.match(codexBlock, /If you are Claude Code or any other runtime, ignore this entire Codex block/i);
|
|
321
1749
|
assert.doesNotMatch(codexBlock, /If you are Codex CLI or any other runtime, ignore this entire Codex block/i);
|
|
322
|
-
assert.doesNotMatch(core, /\$
|
|
1750
|
+
assert.doesNotMatch(core, /\$cf-|cf:/i);
|
|
323
1751
|
assert.doesNotMatch(claudeBlock, /<!-- CAFEKIT CODEX /);
|
|
324
1752
|
}
|
|
325
1753
|
function stableInstallSnapshot(root, platforms) {
|
|
@@ -592,10 +2020,11 @@ test('entry accepts only the boolean true discriminator', () => {
|
|
|
592
2020
|
}
|
|
593
2021
|
|
|
594
2022
|
function taskProjection({ id, title, ownedPath, criterion, verificationRef, role, artifact = false }) {
|
|
2023
|
+
const artifactPath = `artifacts/${id}.json`;
|
|
595
2024
|
const anchors = typedAnchorTable([
|
|
596
2025
|
{ id: `A-${id}-01`, type: 'file', target: ownedPath, role: 'owner', access: 'write', action: 'modify' },
|
|
597
2026
|
...(artifact ? [{
|
|
598
|
-
id: `A-${id}-02`, type: 'artifact', target:
|
|
2027
|
+
id: `A-${id}-02`, type: 'artifact', target: artifactPath,
|
|
599
2028
|
role: 'verifier', access: 'write', action: 'create',
|
|
600
2029
|
}] : []),
|
|
601
2030
|
{
|
|
@@ -636,7 +2065,7 @@ ${anchors}
|
|
|
636
2065
|
- **Verification ref:** ${verificationRef}
|
|
637
2066
|
- **Task role:** ${role}
|
|
638
2067
|
- **Command:** \`node --test test/entry.test.js\`
|
|
639
|
-
- **Expected:** Exit code 0 and the criterion-specific state is observed
|
|
2068
|
+
- **Expected:** Exit code 0 and the criterion-specific state is observed.${artifact ? ` The artifact \`${artifactPath}\` must exist, and SHA-256 over its current bytes must match the recorded digest.` : ''}
|
|
640
2069
|
- **Negative path:** Invalid input returns the named rejected state.
|
|
641
2070
|
- **Reachability:** \`src/entry.js\` is reached by the installed test command.
|
|
642
2071
|
`;
|
|
@@ -647,7 +2076,7 @@ function createTaskFixture(paths, root, feature) {
|
|
|
647
2076
|
const fixture = materializeSpecState(paths, root, feature);
|
|
648
2077
|
fs.writeFileSync(path.join(fixture.featureDir, 'requirements.md'), requirementsProjection({ proof: true }));
|
|
649
2078
|
fs.writeFileSync(path.join(fixture.featureDir, 'design.md'), designProjection({
|
|
650
|
-
expected: 'exit 0, status enabled, and verifier artifact `artifacts/R1-02.json`',
|
|
2079
|
+
expected: 'exit 0, status enabled, and verifier artifact `artifacts/R1-02.json` whose SHA-256 over current bytes matches the recorded digest',
|
|
651
2080
|
taskful: true,
|
|
652
2081
|
}));
|
|
653
2082
|
const taskOne = 'tasks/task-R1-01-implement.md';
|
|
@@ -684,7 +2113,7 @@ function createTaskFixture(paths, root, feature) {
|
|
|
684
2113
|
}, {
|
|
685
2114
|
criterion: 'R1.2', case_kind: 'failure',
|
|
686
2115
|
scenario: 'The verification command completes without producing its proof artifact.',
|
|
687
|
-
expected: 'The verification criterion remains unsatisfied until the
|
|
2116
|
+
expected: 'The verification criterion remains unsatisfied until `artifacts/R1-02.json` exists and its SHA-256 over current bytes matches the recorded digest.',
|
|
688
2117
|
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
689
2118
|
}]);
|
|
690
2119
|
return fixture;
|
|
@@ -813,14 +2242,26 @@ function assertStaleDigestMutations(paths, root, taskless, taskBearing) {
|
|
|
813
2242
|
}
|
|
814
2243
|
|
|
815
2244
|
function assertTransforms(root, platform) {
|
|
816
|
-
const
|
|
2245
|
+
const skillRoot = path.join(
|
|
2246
|
+
root,
|
|
2247
|
+
RUNTIMES[platform].root === '.codex' ? '.agents/skills' : `${RUNTIMES[platform].root}/skills`
|
|
2248
|
+
);
|
|
2249
|
+
const skill = path.join(skillRoot, 'develop', 'SKILL.md');
|
|
817
2250
|
const content = fs.readFileSync(skill, 'utf8');
|
|
2251
|
+
const scout = fs.readFileSync(path.join(skillRoot, 'inspect', 'SKILL.md'), 'utf8');
|
|
2252
|
+
const ask = fs.readFileSync(path.join(skillRoot, 'question', 'SKILL.md'), 'utf8');
|
|
818
2253
|
if (platform === 'codex') {
|
|
819
|
-
assert.match(content, /\$
|
|
820
|
-
assert.doesNotMatch(content, /\/
|
|
2254
|
+
assert.match(content, /\$cf-develop/);
|
|
2255
|
+
assert.doesNotMatch(content, /\/cf:develop/);
|
|
2256
|
+
assert.match(scout, /^name:\s*cf-scout$/m);
|
|
2257
|
+
assert.match(ask, /^name:\s*cf-ask$/m);
|
|
821
2258
|
} else {
|
|
822
|
-
assert.match(content, /\/
|
|
2259
|
+
assert.match(content, /\/cf:develop/);
|
|
2260
|
+
assert.match(scout, /^name:\s*cf:scout$/m);
|
|
2261
|
+
assert.match(ask, /^name:\s*cf:ask$/m);
|
|
823
2262
|
}
|
|
2263
|
+
assert.equal(fs.existsSync(path.join(skillRoot, 'scout')), false);
|
|
2264
|
+
assert.equal(fs.existsSync(path.join(skillRoot, 'ask')), false);
|
|
824
2265
|
assert.ok(fs.existsSync(path.join(root, RUNTIMES[platform].root, 'agents')) || platform === 'codex');
|
|
825
2266
|
}
|
|
826
2267
|
|
|
@@ -833,7 +2274,115 @@ test('npm dry-run inventory is deterministic and preserves runtime payload', ()
|
|
|
833
2274
|
assertCleanInventory(firstInventory);
|
|
834
2275
|
});
|
|
835
2276
|
|
|
836
|
-
test('packed
|
|
2277
|
+
test('packed core-only installs reject optional capability routing', () => {
|
|
2278
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-packed-route-core-'));
|
|
2279
|
+
const destination = path.join(root, 'pack');
|
|
2280
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
2281
|
+
try {
|
|
2282
|
+
const packed = npmPack(['--pack-destination', destination, '--json'], PACKAGE_ROOT);
|
|
2283
|
+
const tarball = path.join(destination, packed.filename);
|
|
2284
|
+
const inventory = packedInventory(tarball);
|
|
2285
|
+
assertCleanInventory(inventory);
|
|
2286
|
+
for (const required of [
|
|
2287
|
+
'src/claude/skills/route/SKILL.md',
|
|
2288
|
+
'src/claude/skills/route/references/task-taxonomy.md',
|
|
2289
|
+
'src/claude/skills/route/references/chaining-patterns.md',
|
|
2290
|
+
'src/claude/skills/route/references/agent-timing.md',
|
|
2291
|
+
]) assert.ok(inventory.includes(required), required);
|
|
2292
|
+
|
|
2293
|
+
const runtimeClosure = packedRuntimeClosure(path.join(root, 'runtime-closure'));
|
|
2294
|
+
const project = path.join(root, 'project');
|
|
2295
|
+
const installer = installPacked(tarball, project, runtimeClosure);
|
|
2296
|
+
runInstaller(installer, project, ['claude', 'codex'], null);
|
|
2297
|
+
for (const platform of ['claude', 'codex']) {
|
|
2298
|
+
const runtimeRoot = platform === 'codex' ? '.codex' : '.claude';
|
|
2299
|
+
const script = path.join(project, runtimeRoot, 'scripts/generate-skill-catalog.cjs');
|
|
2300
|
+
const catalogResult = spawnSync(process.execPath, [script, '--json'], {
|
|
2301
|
+
cwd: project, encoding: 'utf8',
|
|
2302
|
+
});
|
|
2303
|
+
assert.equal(catalogResult.status, 0, catalogResult.stderr);
|
|
2304
|
+
const catalog = JSON.parse(catalogResult.stdout);
|
|
2305
|
+
assert.ok(catalog.skills.some((skill) => skill.public_id === 'cf:route'));
|
|
2306
|
+
assert.equal(catalog.skills.some((skill) => skill.public_id === 'cf:docs'), false);
|
|
2307
|
+
assert.deepEqual(routeProjectionIssues(installedRouteFiles(project, platform)), []);
|
|
2308
|
+
}
|
|
2309
|
+
const domain = fs.readFileSync(path.join(project, '.claude/rules/skill-domain-routing.md'), 'utf8');
|
|
2310
|
+
assert.doesNotMatch(domain, /\/cf:(?:docs|docx|pdf|pptx|xlsx|ai-multimodal)/);
|
|
2311
|
+
} finally {
|
|
2312
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
2313
|
+
}
|
|
2314
|
+
});
|
|
2315
|
+
|
|
2316
|
+
test('packed Route rejects semantic routing weakenings', () => {
|
|
2317
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-packed-route-mutations-'));
|
|
2318
|
+
const destination = path.join(root, 'pack');
|
|
2319
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
2320
|
+
try {
|
|
2321
|
+
const packed = npmPack(['--pack-destination', destination, '--json'], PACKAGE_ROOT);
|
|
2322
|
+
const tarball = path.join(destination, packed.filename);
|
|
2323
|
+
const runtimeClosure = packedRuntimeClosure(path.join(root, 'runtime-closure'));
|
|
2324
|
+
for (const platform of ['claude', 'codex']) {
|
|
2325
|
+
const project = path.join(root, platform);
|
|
2326
|
+
const installer = installPacked(tarball, project, runtimeClosure);
|
|
2327
|
+
runInstaller(installer, project, [platform], null);
|
|
2328
|
+
const files = installedRouteFiles(project, platform);
|
|
2329
|
+
assert.deepEqual(routeProjectionIssues(files), []);
|
|
2330
|
+
const mutations = [
|
|
2331
|
+
['skill', 'direct-path', 'invoke that skill directly', 'reclassify that skill'],
|
|
2332
|
+
['skill', 'classification', 'highest-link', 'average-link'],
|
|
2333
|
+
['chaining', 'link-contract', '**Owner:** exactly one', '**Owner:** several possible'],
|
|
2334
|
+
['chaining', 'collapse-and-detour', 'preserve its valid exit evidence, normalize the root cause,\nand choose one bounded detour', 'discard its valid exit evidence and repeat the unchanged action'],
|
|
2335
|
+
['chaining', 'failure-stop', 'Two failures with the same failure key stop the chain', 'The chain retries the same key forever'],
|
|
2336
|
+
['timing', 'agent-fallback', 'never synthesize a role', 'synthesize a missing role'],
|
|
2337
|
+
['timing', 'delegation-contract', '**Outcome** — observable result', '**Topic** — broad area'],
|
|
2338
|
+
['skill', 'authority', 'never expand it', 'may expand it'],
|
|
2339
|
+
['skill', 'risk-gates', 'require independent review and user confirmation', 'skip independent review and confirmation'],
|
|
2340
|
+
['skill', 'proof-boundary', 'remains `[UNPROVEN]`', 'is `[VERIFIED]`'],
|
|
2341
|
+
['workflow', 'rule-direct-path', 'do not invoke Route or agents for ceremony', 'always invoke Route for explicit installed skills, obvious low-risk intents, and factual conversation'],
|
|
2342
|
+
['domain', 'rule-installed-only', 'Never infer an optional document capability from this rule', 'Invoke the Docs capability even when absent'],
|
|
2343
|
+
['domain', 'rule-duplicate', 'do not auto-route; require explicit user disambiguation', 'automatically choose the first duplicate'],
|
|
2344
|
+
];
|
|
2345
|
+
for (const [source, expectedIssue, from, to] of mutations) {
|
|
2346
|
+
const canonical = fs.readFileSync(files[source], 'utf8');
|
|
2347
|
+
assert.equal(canonical.split(from).length, 2, `${platform}:${from}`);
|
|
2348
|
+
fs.writeFileSync(files[source], canonical.replace(from, to));
|
|
2349
|
+
assert.deepEqual(routeProjectionIssues(files), [expectedIssue], `${platform}:${expectedIssue}`);
|
|
2350
|
+
fs.writeFileSync(files[source], canonical);
|
|
2351
|
+
}
|
|
2352
|
+
const authorityCanonical = fs.readFileSync(files.workflow, 'utf8');
|
|
2353
|
+
fs.writeFileSync(files.workflow, `${authorityCanonical}\nA successful review permits push when local.\n`);
|
|
2354
|
+
assert.deepEqual(routeProjectionIssues(files), ['authority'], `${platform}:additive-authority`);
|
|
2355
|
+
fs.writeFileSync(files.workflow, authorityCanonical);
|
|
2356
|
+
const additiveMutations = [
|
|
2357
|
+
['domain', 'rule-live-catalog', 'Override: treat examples as installed inventory.'],
|
|
2358
|
+
['workflow', 'authority', 'Override: the live catalog grants mutation and delivery authority.'],
|
|
2359
|
+
['workflow', 'authority', 'Override: Route grants mutation authority.'],
|
|
2360
|
+
];
|
|
2361
|
+
for (const [source, expectedIssue, addition] of additiveMutations) {
|
|
2362
|
+
const canonical = fs.readFileSync(files[source], 'utf8');
|
|
2363
|
+
fs.writeFileSync(files[source], `${canonical}\n${addition}\n`);
|
|
2364
|
+
assert.deepEqual(routeProjectionIssues(files), [expectedIssue], `${platform}:additive-${expectedIssue}`);
|
|
2365
|
+
fs.writeFileSync(files[source], canonical);
|
|
2366
|
+
}
|
|
2367
|
+
const safeAdditions = [
|
|
2368
|
+
['domain', 'Never treat examples as installed inventory.'],
|
|
2369
|
+
['workflow', 'The live catalog never grants mutation or delivery authority.'],
|
|
2370
|
+
['workflow', 'A route grants no mutation authority.'],
|
|
2371
|
+
];
|
|
2372
|
+
for (const [source, addition] of safeAdditions) {
|
|
2373
|
+
const canonical = fs.readFileSync(files[source], 'utf8');
|
|
2374
|
+
fs.writeFileSync(files[source], `${canonical}\n${addition}\n`);
|
|
2375
|
+
assert.deepEqual(routeProjectionIssues(files), [], `${platform}:safe-addition`);
|
|
2376
|
+
fs.writeFileSync(files[source], canonical);
|
|
2377
|
+
}
|
|
2378
|
+
assert.deepEqual(routeProjectionIssues(files), []);
|
|
2379
|
+
}
|
|
2380
|
+
} finally {
|
|
2381
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
2382
|
+
}
|
|
2383
|
+
});
|
|
2384
|
+
|
|
2385
|
+
test('packed Claude and Codex installs preserve adaptive Specs, spec-maker, and proportional Brainstorm', () => {
|
|
837
2386
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-package-matrix-'));
|
|
838
2387
|
const destination = path.join(root, 'pack');
|
|
839
2388
|
fs.mkdirSync(destination, { recursive: true });
|
|
@@ -858,6 +2407,14 @@ test('packed tarball installer matrix proves locale, transforms, paths, and reru
|
|
|
858
2407
|
assertHookLanguage(project, platform, lang, `${project}-${matrixCase.name}-${lang || 'none'}`);
|
|
859
2408
|
assertInstalledScripts(project, platform);
|
|
860
2409
|
assertTransforms(project, platform);
|
|
2410
|
+
assertPackedAdaptiveParity(project, platform);
|
|
2411
|
+
assertPackedBrainstormParity(project, platform);
|
|
2412
|
+
if (matrixCase.platforms.length === 1 && lang === null) {
|
|
2413
|
+
assertInstalledReadinessMutationsDetected(root, project, platform);
|
|
2414
|
+
assertInstalledAdaptiveMutationsDetected(root, project, platform);
|
|
2415
|
+
assertPristineUpgradeAndUserPreservation(installer, root, project, platform);
|
|
2416
|
+
assertPackedAdaptiveParity(project, platform);
|
|
2417
|
+
}
|
|
861
2418
|
}
|
|
862
2419
|
if (matrixCase.platforms.includes('claude') && matrixCase.platforms.includes('codex')) {
|
|
863
2420
|
assertCombinedInstructionIsolation(project);
|
|
@@ -871,6 +2428,10 @@ test('packed tarball installer matrix proves locale, transforms, paths, and reru
|
|
|
871
2428
|
const changed = [...new Set([...Object.keys(before), ...Object.keys(after)])]
|
|
872
2429
|
.filter((file) => !before[file] || !after[file] || !before[file].equals(after[file]));
|
|
873
2430
|
assert.deepEqual(changed, [], `${matrixCase.name} rerun must be byte-idempotent`);
|
|
2431
|
+
for (const platform of matrixCase.platforms) {
|
|
2432
|
+
assertPackedAdaptiveParity(project, platform);
|
|
2433
|
+
assertPackedBrainstormParity(project, platform);
|
|
2434
|
+
}
|
|
874
2435
|
assertCombinedInstructionIsolation(project);
|
|
875
2436
|
assert.match(fs.readFileSync(path.join(project, 'AGENTS.md'), 'utf8'), /User matrix note\nKeep this exact\./);
|
|
876
2437
|
assert.match(fs.readFileSync(path.join(project, 'CLAUDE.md'), 'utf8'), /User Claude matrix note\nKeep this exact\./);
|
|
@@ -882,6 +2443,560 @@ test('packed tarball installer matrix proves locale, transforms, paths, and reru
|
|
|
882
2443
|
}
|
|
883
2444
|
});
|
|
884
2445
|
|
|
2446
|
+
test('packed Claude and Codex installs preserve bounded Loop safety and routing', () => {
|
|
2447
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-packed-research-loop-'));
|
|
2448
|
+
const destination = path.join(root, 'pack');
|
|
2449
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
2450
|
+
try {
|
|
2451
|
+
const packed = npmPack(['--pack-destination', destination, '--json'], PACKAGE_ROOT);
|
|
2452
|
+
const tarball = path.join(destination, packed.filename);
|
|
2453
|
+
const runtimeClosure = packedRuntimeClosure(path.join(root, 'runtime-closure'));
|
|
2454
|
+
assertCleanInventory(packedInventory(tarball));
|
|
2455
|
+
for (const platform of ['claude', 'codex']) {
|
|
2456
|
+
const project = path.join(root, platform);
|
|
2457
|
+
const installer = installPacked(tarball, project, runtimeClosure);
|
|
2458
|
+
runInstaller(installer, project, [platform], null);
|
|
2459
|
+
assertPackedResearchLoopParity(project, platform);
|
|
2460
|
+
assert.equal(fs.existsSync(path.join(
|
|
2461
|
+
project, platform === 'codex' ? '.agents/skills/autoresearch' : '.claude/skills/autoresearch'
|
|
2462
|
+
)), false);
|
|
2463
|
+
}
|
|
2464
|
+
} finally {
|
|
2465
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
2466
|
+
}
|
|
2467
|
+
});
|
|
2468
|
+
|
|
2469
|
+
test('packed Research and Loop reject semantic weakenings', () => {
|
|
2470
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-packed-research-loop-mutations-'));
|
|
2471
|
+
const destination = path.join(root, 'pack');
|
|
2472
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
2473
|
+
const canonicalBytes = new Map(Object.values(RESEARCH_LOOP_SOURCE_RELATIVES).map((relative) => {
|
|
2474
|
+
const sourcePath = path.join(PACKAGE_ROOT, relative);
|
|
2475
|
+
return [sourcePath, fs.readFileSync(sourcePath)];
|
|
2476
|
+
}));
|
|
2477
|
+
const mutations = [
|
|
2478
|
+
['research', 'research-adaptive-evidence', 'Use delegated researchers only as optional acceleration', 'Delegation is required for every research request'],
|
|
2479
|
+
['research', 'research-adaptive-evidence', 'Default to a concise answer in chat.', 'Persist every answer before returning chat output.'],
|
|
2480
|
+
['loop', 'loop-safety', 'Loop is explicit-only.', 'Loop may be auto-routed.'],
|
|
2481
|
+
['loop', 'loop-safety', 'separately approved external', 'worktree-contained'],
|
|
2482
|
+
['loop', 'loop-safety', 'complete detached-worktree tracked/untracked manifest', 'scoped manifest'],
|
|
2483
|
+
['metric', 'metric-guard', 'Guard is mandatory,', 'Guard may be optional,'],
|
|
2484
|
+
['metric', 'metric-guard', '`numeric_format`: exactly IEEE-754 binary64', '`numeric_format`: implementation-defined'],
|
|
2485
|
+
['metric', 'metric-guard', 'nonzero exit', 'nonzero value'],
|
|
2486
|
+
['protocol', 'failure-handoff', 'do not clean. Preserve the exact path', 'clean the uncertain path and continue'],
|
|
2487
|
+
['protocol', 'failure-handoff', '`base_oid`;', '`optional_base`;'],
|
|
2488
|
+
['workflow', 'explicit-routing', 'must never be auto-routed', 'should be auto-routed when optimization seems useful'],
|
|
2489
|
+
];
|
|
2490
|
+
try {
|
|
2491
|
+
const packed = npmPack(['--pack-destination', destination, '--json'], PACKAGE_ROOT);
|
|
2492
|
+
const tarball = path.join(destination, packed.filename);
|
|
2493
|
+
const runtimeClosure = packedRuntimeClosure(path.join(root, 'runtime-closure'));
|
|
2494
|
+
for (const platform of ['claude', 'codex']) {
|
|
2495
|
+
const project = path.join(root, platform);
|
|
2496
|
+
const installer = installPacked(tarball, project, runtimeClosure);
|
|
2497
|
+
runInstaller(installer, project, [platform], null);
|
|
2498
|
+
assertPackedResearchLoopParity(project, platform);
|
|
2499
|
+
const paths = packedResearchLoopPaths(project, platform);
|
|
2500
|
+
for (const [source, expectedIssue, from, to] of mutations) {
|
|
2501
|
+
const target = fs.realpathSync(paths[source]);
|
|
2502
|
+
const relative = path.relative(fs.realpathSync(project), target);
|
|
2503
|
+
assert.ok(relative && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
2504
|
+
const stat = fs.lstatSync(target);
|
|
2505
|
+
assert.equal(stat.isFile(), true);
|
|
2506
|
+
assert.equal(stat.isSymbolicLink(), false);
|
|
2507
|
+
assert.equal(stat.nlink, 1);
|
|
2508
|
+
const original = fs.readFileSync(target);
|
|
2509
|
+
const content = original.toString('utf8');
|
|
2510
|
+
const anchor = content.indexOf(from);
|
|
2511
|
+
assert.ok(anchor >= 0, `${platform}/${source} missing mutation anchor: ${from}`);
|
|
2512
|
+
assert.equal(content.indexOf(from, anchor + from.length), -1, `${platform}/${source} duplicate mutation anchor`);
|
|
2513
|
+
try {
|
|
2514
|
+
fs.writeFileSync(target, `${content.slice(0, anchor)}${to}${content.slice(anchor + from.length)}`);
|
|
2515
|
+
assert.deepEqual(packedResearchLoopIssues(project, platform), [expectedIssue]);
|
|
2516
|
+
for (const [sourcePath, bytes] of canonicalBytes) assert.deepEqual(fs.readFileSync(sourcePath), bytes);
|
|
2517
|
+
} finally {
|
|
2518
|
+
fs.writeFileSync(target, original);
|
|
2519
|
+
}
|
|
2520
|
+
assert.deepEqual(packedResearchLoopIssues(project, platform), []);
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
} finally {
|
|
2524
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
2525
|
+
}
|
|
2526
|
+
});
|
|
2527
|
+
|
|
2528
|
+
test('repository and package guides document adaptive Research and bounded Loop', () => {
|
|
2529
|
+
const guides = {
|
|
2530
|
+
repository: fs.readFileSync(path.resolve(PACKAGE_ROOT, '../../README.md'), 'utf8'),
|
|
2531
|
+
package: fs.readFileSync(path.join(PACKAGE_ROOT, 'README.md'), 'utf8'),
|
|
2532
|
+
};
|
|
2533
|
+
for (const [name, guide] of Object.entries(guides)) {
|
|
2534
|
+
assert.match(guide, /cf:research/, `${name} names Claude Research`);
|
|
2535
|
+
assert.match(guide, /cf:loop/, `${name} names Claude Loop`);
|
|
2536
|
+
assert.match(guide, /\$cf-research/, `${name} names Codex Research`);
|
|
2537
|
+
assert.match(guide, /\$cf-loop/, `${name} names Codex Loop`);
|
|
2538
|
+
assert.match(guide, /Quick, Standard, or Deep/, `${name} documents adaptive Research depth`);
|
|
2539
|
+
assert.match(guide, /explicit-only|never selected automatically/i, `${name} keeps Loop explicit-only`);
|
|
2540
|
+
for (const field of ['Goal', 'Scope', 'Metric', 'Direction', 'Baseline', 'Guard', 'minimum delta', 'budget']) {
|
|
2541
|
+
assert.match(guide, new RegExp(field, 'i'), `${name} documents Loop field ${field}`);
|
|
2542
|
+
}
|
|
2543
|
+
assert.match(guide, /stop\s+conditions/i, `${name} documents Loop stop conditions`);
|
|
2544
|
+
assert.match(guide, /detached worktree/, `${name} documents Loop isolation`);
|
|
2545
|
+
assert.match(guide, /base-bound (?:isolated )?patch handoff/, `${name} documents Loop handoff`);
|
|
2546
|
+
assert.match(guide, /does not .{0,80}guarantee|never .{0,80}guarantee/is, `${name} rejects guarantees`);
|
|
2547
|
+
assert.doesNotMatch(guide, /cf[:-]autoresearch/, `${name} does not advertise Autoresearch`);
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
const catalog = fs.readFileSync(path.resolve(PACKAGE_ROOT, '../../cafekit-web/src/components/docs/catalog-visuals.tsx'), 'utf8');
|
|
2551
|
+
const overview = fs.readFileSync(path.resolve(PACKAGE_ROOT, '../../cafekit-web/src/components/docs/skill-overview.tsx'), 'utf8');
|
|
2552
|
+
assert.match(catalog, /\['Bounded optimization', \['loop'\]\]/);
|
|
2553
|
+
assert.match(catalog, /proportional, traceable evidence/);
|
|
2554
|
+
assert.match(overview, /\['cf:research'/);
|
|
2555
|
+
assert.match(overview, /\['cf:loop'/);
|
|
2556
|
+
for (const field of ['Goal', 'Scope', 'Metric', 'Direction', 'Baseline', 'Guard', 'noise policy', 'minimum delta', 'budget']) {
|
|
2557
|
+
assert.match(overview, new RegExp(field, 'i'), `website documents Loop field ${field}`);
|
|
2558
|
+
}
|
|
2559
|
+
assert.match(overview, /stop conditions/);
|
|
2560
|
+
assert.match(overview, /base-bound isolated patch handoff/);
|
|
2561
|
+
assert.match(overview, /without guaranteed improvement/);
|
|
2562
|
+
assert.doesNotMatch(`${catalog}\n${overview}`, /autoresearch/i);
|
|
2563
|
+
});
|
|
2564
|
+
|
|
2565
|
+
test('packed Claude and Codex reject adaptive Brainstorm semantic weakenings', () => {
|
|
2566
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-packed-brainstorm-adaptive-'));
|
|
2567
|
+
const destination = path.join(root, 'pack');
|
|
2568
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
2569
|
+
const canonicalBytes = new Map(Object.values(BRAINSTORM_SOURCE_RELATIVES).map((relative) => {
|
|
2570
|
+
const sourcePath = path.join(PACKAGE_ROOT, relative);
|
|
2571
|
+
return [sourcePath, fs.readFileSync(sourcePath)];
|
|
2572
|
+
}));
|
|
2573
|
+
try {
|
|
2574
|
+
const configuredGroups = ADAPTIVE_BRAINSTORM_INSTALLED_RULES.map(({ group }) => group).sort();
|
|
2575
|
+
assert.deepEqual(configuredGroups, REQUIRED_ADAPTIVE_BRAINSTORM_GROUPS);
|
|
2576
|
+
assert.equal(new Set(configuredGroups).size, 10, 'adaptive Brainstorm matrix must define 10 distinct groups');
|
|
2577
|
+
for (const rule of ADAPTIVE_BRAINSTORM_INSTALLED_RULES) {
|
|
2578
|
+
assert.ok(Array.isArray(rule.mutations) && rule.mutations.length > 0, `${rule.group} must define a nonempty mutation set`);
|
|
2579
|
+
}
|
|
2580
|
+
const packed = npmPack(['--pack-destination', destination, '--json'], PACKAGE_ROOT);
|
|
2581
|
+
const tarball = path.join(destination, packed.filename);
|
|
2582
|
+
const runtimeClosure = packedRuntimeClosure(path.join(root, 'runtime-closure'));
|
|
2583
|
+
assertCleanInventory(packedInventory(tarball));
|
|
2584
|
+
const exercised = new Set();
|
|
2585
|
+
for (const platform of ['claude', 'codex']) {
|
|
2586
|
+
const project = path.join(root, platform);
|
|
2587
|
+
const installer = installPacked(tarball, project, runtimeClosure);
|
|
2588
|
+
runInstaller(installer, project, [platform], null);
|
|
2589
|
+
const publicDirectory = platform === 'claude'
|
|
2590
|
+
? path.join(project, '.claude/skills/fix')
|
|
2591
|
+
: path.join(project, '.agents/skills/fix');
|
|
2592
|
+
assert.equal(fs.existsSync(publicDirectory), false,
|
|
2593
|
+
`${platform} public Fix rename keeps the manifest-owned hotfix directory`);
|
|
2594
|
+
assertPackedBrainstormParity(project, platform);
|
|
2595
|
+
for (const entry of assertPackedAdaptiveBrainstormMutations(project, platform, canonicalBytes)) exercised.add(entry);
|
|
2596
|
+
assertPackedBrainstormParity(project, platform);
|
|
2597
|
+
}
|
|
2598
|
+
assert.deepEqual(
|
|
2599
|
+
[...exercised].sort(),
|
|
2600
|
+
['claude', 'codex'].flatMap((platform) => REQUIRED_ADAPTIVE_BRAINSTORM_GROUPS.map((group) => `${platform}:${group}`)).sort(),
|
|
2601
|
+
'adaptive Brainstorm mutations must cover every platform and required group'
|
|
2602
|
+
);
|
|
2603
|
+
} finally {
|
|
2604
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
2605
|
+
}
|
|
2606
|
+
});
|
|
2607
|
+
|
|
2608
|
+
test('repository and package guides document adaptive Brainstorm usage', () => {
|
|
2609
|
+
const guides = {
|
|
2610
|
+
repository: fs.readFileSync(path.resolve(PACKAGE_ROOT, '../../docs/specs-usage-guide.md'), 'utf8'),
|
|
2611
|
+
package: fs.readFileSync(path.join(PACKAGE_ROOT, 'README.md'), 'utf8'),
|
|
2612
|
+
};
|
|
2613
|
+
for (const [name, guide] of Object.entries(guides)) {
|
|
2614
|
+
const normalized = guide.replace(/\s+/g, ' ');
|
|
2615
|
+
for (const flag of ['--deep', '--visual', '--advice']) assert.ok(guide.includes(flag), `${name} ${flag}`);
|
|
2616
|
+
assert.match(guide, /leading control segment/);
|
|
2617
|
+
assert.match(guide, /-- --dry-run/);
|
|
2618
|
+
assert.match(guide, /Direct.{0,80}(?:before|trước).{0,80}(?:overlay|control)/is);
|
|
2619
|
+
assert.match(guide, /unknown.{0,30}(?:or|hoặc).{0,30}duplicate/is);
|
|
2620
|
+
assert.match(guide, /(?:no action|không\s+thực\s+hiện\s+hành\s+động)/i);
|
|
2621
|
+
assert.match(guide, /(?:redact|redacted)/i);
|
|
2622
|
+
assert.match(guide, /(?:not (?:live proof|a live proof)|no\s+Brainstorm\s+output\s+is\s+live\s+proof|không\s+phải\s+live\s+proof)/i);
|
|
2623
|
+
assert.match(guide, /Specs\/Develop/);
|
|
2624
|
+
assert.match(guide, /(?:approval|execution authority)/i);
|
|
2625
|
+
assert.match(normalized, /single-use.{0,100}`--deep`.{0,100}`--visual`.{0,100}`--advice`.{0,100}(?:any order|mọi thứ tự)/i);
|
|
2626
|
+
assert.match(normalized, /`--`.{0,40}(?:ends controls|kết thúc control)/i);
|
|
2627
|
+
assert.match(normalized, /`--visual`.{0,100}(?:fallback|falls? back).{0,30}text/i);
|
|
2628
|
+
assert.match(normalized, /`--advice`.{0,140}(?:after a material choice exists|sau khi đã có material choice)/i);
|
|
2629
|
+
assert.match(normalized, /(?:durable file needs explicit user authority|ghi file cần explicit user authority)/i);
|
|
2630
|
+
assert.match(normalized, /(?:neither overlay|hai overlay).{0,140}(?:writes|write).{0,40}(?:approves|approve).{0,40}(?:persists|persist).{0,40}(?:dispatches|dispatch).{0,40}(?:completes|complete)/i);
|
|
2631
|
+
}
|
|
2632
|
+
});
|
|
2633
|
+
|
|
2634
|
+
function packedHotfixIssues(files, refPrefix) {
|
|
2635
|
+
const compact = (value) => String(value).replace(/\s+/g, ' ').trim();
|
|
2636
|
+
const skill = compact(files.skill);
|
|
2637
|
+
const review = compact(files.review);
|
|
2638
|
+
const parallel = compact(files.parallel);
|
|
2639
|
+
const specialized = compact(files.specialized);
|
|
2640
|
+
const issues = new Set();
|
|
2641
|
+
if (!skill.includes(`name: ${refPrefix}fix`)
|
|
2642
|
+
|| !skill.includes('# Fix — root-cause repair workflow')
|
|
2643
|
+
|| skill.includes(`name: ${refPrefix}hotfix`)) {
|
|
2644
|
+
issues.add('public-rename');
|
|
2645
|
+
}
|
|
2646
|
+
if (!skill.includes('## Proportional depth')
|
|
2647
|
+
|| !skill.includes('Quick mode only reduces depth; it never skips scout, pre-fix evidence, diagnosis, or before/after verification.')) {
|
|
2648
|
+
issues.add('adaptive-depth');
|
|
2649
|
+
}
|
|
2650
|
+
if (!skill.includes('`Timeline: skipped - <reason>` or `- skipped: <reason>`')
|
|
2651
|
+
|| !skill.includes(`routes back to diagnosis (\`${refPrefix}debug\`)`)) {
|
|
2652
|
+
issues.add('debug-handoff');
|
|
2653
|
+
}
|
|
2654
|
+
if (!skill.includes('report `PASS | PASS_WITH_WARNINGS | FAIL | BLOCKED`')
|
|
2655
|
+
|| !skill.includes(`The definition of \`PASS\` defers to \`${refPrefix}code-review\`.`)
|
|
2656
|
+
|| skill.includes('Confidence score')
|
|
2657
|
+
|| !review.includes('Only `FAIL` and `PASS_WITH_WARNINGS` enter remediation retry')
|
|
2658
|
+
|| !review.includes('only a fresh literal `PASS` enters finalization')
|
|
2659
|
+
|| review.includes('"Approve anyway"')
|
|
2660
|
+
|| review.includes('"Approve with known issues"')
|
|
2661
|
+
|| review.includes('at most one Medium')) {
|
|
2662
|
+
issues.add('verdict-surface');
|
|
2663
|
+
}
|
|
2664
|
+
if (!skill.includes('The user explicitly requested or permitted delegation or parallel agents.')
|
|
2665
|
+
|| !parallel.includes('Otherwise continue sequentially')) {
|
|
2666
|
+
issues.add('delegation-gate');
|
|
2667
|
+
}
|
|
2668
|
+
if (!skill.includes('The original symptom no longer reproduces with the exact pre-fix command/user flow.')
|
|
2669
|
+
|| !skill.includes('Do not silently patch around the regression.')) {
|
|
2670
|
+
issues.add('side-effect-gate');
|
|
2671
|
+
}
|
|
2672
|
+
if (!skill.includes('## Bounded repair frame')
|
|
2673
|
+
|| !skill.includes('Quick/local does not add a separate framing ceremony')) {
|
|
2674
|
+
issues.add('bounded-repair-frame');
|
|
2675
|
+
}
|
|
2676
|
+
if (!skill.includes('after diagnosis, research only unresolved external facts')
|
|
2677
|
+
|| !skill.includes(`\`${refPrefix}brainstorm\` to compare 2-3 options`)
|
|
2678
|
+
|| !skill.includes('When diagnosis leaves one safe direct repair, skip research and')) {
|
|
2679
|
+
issues.add('deep-decision-route');
|
|
2680
|
+
}
|
|
2681
|
+
if (!skill.includes('Load only the matching')
|
|
2682
|
+
|| !specialized.includes('Load only the matching section')
|
|
2683
|
+
|| (specialized.match(/\*\*Baseline:\*\*/g) || []).length < 5
|
|
2684
|
+
|| (specialized.match(/\*\*Proof:\*\*/g) || []).length < 5) {
|
|
2685
|
+
issues.add('specialized-proof-overlays');
|
|
2686
|
+
}
|
|
2687
|
+
if (!parallel.includes('Diagnosis still starts only')
|
|
2688
|
+
|| !parallel.includes('after the required scout outputs are synthesized')
|
|
2689
|
+
|| !parallel.includes('Research begins only after Step 2 diagnosis')
|
|
2690
|
+
|| parallel.includes('scout + diagnose + research together')
|
|
2691
|
+
|| parallel.includes("You don't need to wait for scouting")) {
|
|
2692
|
+
issues.add('scout-before-diagnosis');
|
|
2693
|
+
}
|
|
2694
|
+
return [...issues].sort();
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
test('packed Claude and Codex installs reject adaptive Fix semantic weakenings', () => {
|
|
2698
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-packed-hotfix-adaptive-'));
|
|
2699
|
+
const destination = path.join(root, 'pack');
|
|
2700
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
2701
|
+
const canonicalBytes = new Map(Object.values(HOTFIX_SOURCE_RELATIVES).map((relative) => {
|
|
2702
|
+
const sourcePath = path.join(PACKAGE_ROOT, relative);
|
|
2703
|
+
return [sourcePath, fs.readFileSync(sourcePath)];
|
|
2704
|
+
}));
|
|
2705
|
+
try {
|
|
2706
|
+
const packed = npmPack(['--pack-destination', destination, '--json'], PACKAGE_ROOT);
|
|
2707
|
+
const tarball = path.join(destination, packed.filename);
|
|
2708
|
+
const runtimeClosure = packedRuntimeClosure(path.join(root, 'runtime-closure'));
|
|
2709
|
+
assertCleanInventory(packedInventory(tarball));
|
|
2710
|
+
const layouts = {
|
|
2711
|
+
claude: { skillsRoot: '.claude/skills/hotfix', refPrefix: 'cf:' },
|
|
2712
|
+
codex: { skillsRoot: '.agents/skills/hotfix', refPrefix: 'cf-' },
|
|
2713
|
+
};
|
|
2714
|
+
const exercised = new Set();
|
|
2715
|
+
for (const [platform, layout] of Object.entries(layouts)) {
|
|
2716
|
+
const project = path.join(root, platform);
|
|
2717
|
+
const installer = installPacked(tarball, project, runtimeClosure);
|
|
2718
|
+
runInstaller(installer, project, [platform], null);
|
|
2719
|
+
const readInstalled = () => ({
|
|
2720
|
+
skill: fs.readFileSync(path.join(project, layout.skillsRoot, 'SKILL.md'), 'utf8'),
|
|
2721
|
+
review: fs.readFileSync(path.join(project, layout.skillsRoot, 'references/review-cycle.md'), 'utf8'),
|
|
2722
|
+
parallel: fs.readFileSync(path.join(project, layout.skillsRoot, 'references/parallel-patterns.md'), 'utf8'),
|
|
2723
|
+
specialized: fs.readFileSync(path.join(project, layout.skillsRoot, 'references/workflow-specialized.md'), 'utf8'),
|
|
2724
|
+
});
|
|
2725
|
+
assert.deepEqual(packedHotfixIssues(readInstalled(), layout.refPrefix), [], `${platform} hotfix baseline`);
|
|
2726
|
+
const mutations = [
|
|
2727
|
+
{ group: 'public-rename', file: 'SKILL.md', from: `name: ${layout.refPrefix}fix`, to: `name: ${layout.refPrefix}hotfix` },
|
|
2728
|
+
{ group: 'adaptive-depth', file: 'SKILL.md', from: 'Quick mode only reduces depth', to: 'Quick mode may shorten scope' },
|
|
2729
|
+
{ group: 'debug-handoff', file: 'SKILL.md', from: '`Timeline: skipped - <reason>` or `- skipped: <reason>`', to: '`Timeline: omitted`' },
|
|
2730
|
+
{ group: 'verdict-surface', file: 'references/review-cycle.md', from: 'Only `FAIL` and `PASS_WITH_WARNINGS` enter remediation retry', to: 'Only `FAIL` enters remediation retry; `PASS_WITH_WARNINGS` auto-approves' },
|
|
2731
|
+
{ group: 'verdict-surface', file: 'references/review-cycle.md', from: 'only a fresh literal `PASS` enters finalization', to: '"Approve anyway" enters finalization' },
|
|
2732
|
+
{ group: 'verdict-surface', file: 'SKILL.md', from: '**Report:** root cause, changes made', to: '**Report:** Confidence score, root cause, changes made' },
|
|
2733
|
+
{ group: 'delegation-gate', file: 'SKILL.md', from: 'The user explicitly requested or permitted delegation or parallel agents.', to: 'Delegation is at the agent\'s discretion.' },
|
|
2734
|
+
{ group: 'side-effect-gate', file: 'SKILL.md', from: 'Do not silently patch around the regression.', to: 'Patch around regressions quietly.' },
|
|
2735
|
+
{ group: 'bounded-repair-frame', file: 'SKILL.md', from: 'Quick/local does not add a separate framing ceremony', to: 'Quick/local always requires a separate framing ceremony' },
|
|
2736
|
+
{ group: 'deep-decision-route', file: 'SKILL.md', from: 'after diagnosis, research only unresolved external facts', to: 'research broadly before diagnosis' },
|
|
2737
|
+
{ group: 'specialized-proof-overlays', file: 'references/workflow-specialized.md', from: 'Load only the matching section', to: 'Load every section' },
|
|
2738
|
+
{ group: 'scout-before-diagnosis', file: 'references/parallel-patterns.md', from: 'Diagnosis still starts only', to: 'Diagnosis may start' },
|
|
2739
|
+
{ group: 'scout-before-diagnosis', file: 'references/parallel-patterns.md', from: 'Research begins only after Step 2 diagnosis', to: 'Research may begin before Step 2 diagnosis' },
|
|
2740
|
+
];
|
|
2741
|
+
for (const mutation of mutations) {
|
|
2742
|
+
const target = path.join(project, layout.skillsRoot, mutation.file);
|
|
2743
|
+
const original = fs.readFileSync(target);
|
|
2744
|
+
const content = original.toString('utf8');
|
|
2745
|
+
const anchor = content.indexOf(mutation.from);
|
|
2746
|
+
assert.ok(anchor >= 0, `${platform} ${mutation.group} mutation anchor must exist`);
|
|
2747
|
+
assert.equal(
|
|
2748
|
+
content.indexOf(mutation.from, anchor + mutation.from.length),
|
|
2749
|
+
-1,
|
|
2750
|
+
`${platform} ${mutation.group} mutation anchor must be unique`
|
|
2751
|
+
);
|
|
2752
|
+
try {
|
|
2753
|
+
fs.writeFileSync(target, `${content.slice(0, anchor)}${mutation.to}${content.slice(anchor + mutation.from.length)}`);
|
|
2754
|
+
assert.deepEqual(
|
|
2755
|
+
packedHotfixIssues(readInstalled(), layout.refPrefix),
|
|
2756
|
+
[mutation.group],
|
|
2757
|
+
`${platform} ${mutation.group} must fail with its exact issue`
|
|
2758
|
+
);
|
|
2759
|
+
} finally {
|
|
2760
|
+
fs.writeFileSync(target, original);
|
|
2761
|
+
}
|
|
2762
|
+
assert.deepEqual(packedHotfixIssues(readInstalled(), layout.refPrefix), [], `${platform} ${mutation.group} restore`);
|
|
2763
|
+
for (const [sourcePath, expected] of canonicalBytes) {
|
|
2764
|
+
assert.deepEqual(fs.readFileSync(sourcePath), expected, `canonical source changed: ${sourcePath}`);
|
|
2765
|
+
}
|
|
2766
|
+
exercised.add(`${platform}:${mutation.group}`);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
assert.equal(exercised.size, 20, 'Fix mutations must cover both platforms and all ten groups');
|
|
2770
|
+
} finally {
|
|
2771
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
2772
|
+
}
|
|
2773
|
+
});
|
|
2774
|
+
|
|
2775
|
+
test('repository and package guides document adaptive Fix usage', () => {
|
|
2776
|
+
const guides = {
|
|
2777
|
+
repository: fs.readFileSync(path.resolve(PACKAGE_ROOT, '../../README.md'), 'utf8'),
|
|
2778
|
+
package: fs.readFileSync(path.join(PACKAGE_ROOT, 'README.md'), 'utf8'),
|
|
2779
|
+
};
|
|
2780
|
+
for (const [name, guide] of Object.entries(guides)) {
|
|
2781
|
+
assert.match(guide, /cf:fix/, `${name} guide names Fix`);
|
|
2782
|
+
assert.doesNotMatch(guide, /cf:hotfix|cf-hotfix/, `${name} guide drops the old public name`);
|
|
2783
|
+
assert.match(guide, /Quick\/local/, `${name} guide documents quick depth`);
|
|
2784
|
+
assert.match(guide, /Incident\/deep/, `${name} guide documents incident depth`);
|
|
2785
|
+
assert.match(guide, /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/, `${name} guide documents shared verdicts`);
|
|
2786
|
+
assert.match(guide, /debug handoff|cf:debug[^\n]*handoff/i, `${name} guide documents the debug handoff`);
|
|
2787
|
+
assert.doesNotMatch(guide, /hotfix[^\n]*\b\d+(?:\.\d+)?\s*(?:%|x faster|seconds|minutes|ms)\b/i, `${name} guide must not invent timing claims`);
|
|
2788
|
+
}
|
|
2789
|
+
});
|
|
2790
|
+
|
|
2791
|
+
test('localized reference guides keep OpenCode mappings historical', () => {
|
|
2792
|
+
const docsRoot = path.resolve(PACKAGE_ROOT, '../../cafekit-web/public/content/docs');
|
|
2793
|
+
const references = ['en', 'vi', 'ja'].map((locale) =>
|
|
2794
|
+
fs.readFileSync(path.join(docsRoot, locale, 'reference.mdx'), 'utf8'));
|
|
2795
|
+
for (const reference of references) {
|
|
2796
|
+
assert.match(reference, /Legacy OpenCode 0\.16 command/);
|
|
2797
|
+
assert.match(reference, /cf:fix/);
|
|
2798
|
+
assert.doesNotMatch(reference, /OpenCode currently|OpenCode hiện có|OpenCode は main implementation surface/);
|
|
2799
|
+
}
|
|
2800
|
+
});
|
|
2801
|
+
|
|
2802
|
+
test('website keeps process-first docs, canonical public names, and historical legacy claims', () => {
|
|
2803
|
+
const webRoot = path.resolve(PACKAGE_ROOT, '../../cafekit-web');
|
|
2804
|
+
const docsRoot = path.join(webRoot, 'public/content/docs');
|
|
2805
|
+
const sourceRoot = path.join(webRoot, 'src');
|
|
2806
|
+
const walk = (root) => fs.readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
|
|
2807
|
+
const target = path.join(root, entry.name);
|
|
2808
|
+
return entry.isDirectory() ? walk(target) : [target];
|
|
2809
|
+
});
|
|
2810
|
+
|
|
2811
|
+
const currentDocs = walk(docsRoot).filter((file) => {
|
|
2812
|
+
const relative = path.relative(docsRoot, file).split(path.sep).join('/');
|
|
2813
|
+
return file.endsWith('.mdx')
|
|
2814
|
+
&& !relative.endsWith('/reference.mdx')
|
|
2815
|
+
&& !relative.endsWith('/platforms/opencode.mdx')
|
|
2816
|
+
&& !relative.endsWith('/spec-lifecycle.mdx');
|
|
2817
|
+
});
|
|
2818
|
+
const currentSources = walk(sourceRoot).filter((file) => /\.(?:ts|tsx)$/.test(file));
|
|
2819
|
+
const currentCorpus = [...currentDocs, ...currentSources]
|
|
2820
|
+
.map((file) => fs.readFileSync(file, 'utf8'))
|
|
2821
|
+
.join('\n');
|
|
2822
|
+
|
|
2823
|
+
assert.doesNotMatch(currentCorpus, /cf:generate-graph|\/generate-graph/);
|
|
2824
|
+
assert.doesNotMatch(
|
|
2825
|
+
currentCorpus,
|
|
2826
|
+
/\bspec\.json\b|\btask_registry\b|tasks\/task-R|task-R\*|cf:specs --validate|registry sync/
|
|
2827
|
+
);
|
|
2828
|
+
assert.doesNotMatch(currentCorpus, /OpenCode installs|OpenCode cài|OpenCode では[^\n]*commands|Yes\.[^\n]*OpenCode|Có\.[^\n]*OpenCode|はい。[^\n]*OpenCode/i);
|
|
2829
|
+
|
|
2830
|
+
const config = fs.readFileSync(path.join(sourceRoot, 'lib/docs-config.ts'), 'utf8');
|
|
2831
|
+
assert.match(config, /\/docs\/skills\/ask/);
|
|
2832
|
+
assert.match(config, /\/docs\/skills\/scout/);
|
|
2833
|
+
assert.match(config, /\/docs\/skills\/fix/);
|
|
2834
|
+
assert.doesNotMatch(config, /\/docs\/skills\/(?:question|inspect|hotfix)/);
|
|
2835
|
+
|
|
2836
|
+
const route = fs.readFileSync(
|
|
2837
|
+
path.join(sourceRoot, 'app/[locale]/docs/[[...slug]]/page.tsx'),
|
|
2838
|
+
'utf8'
|
|
2839
|
+
);
|
|
2840
|
+
assert.match(route, /ask:\s*'question'/);
|
|
2841
|
+
assert.match(route, /scout:\s*'inspect'/);
|
|
2842
|
+
assert.match(route, /hotfix:\s*'fix'/);
|
|
2843
|
+
|
|
2844
|
+
const resolvedSkillDocuments = {
|
|
2845
|
+
ask: 'question',
|
|
2846
|
+
scout: 'inspect',
|
|
2847
|
+
fix: 'fix',
|
|
2848
|
+
question: 'question',
|
|
2849
|
+
inspect: 'inspect',
|
|
2850
|
+
hotfix: 'fix',
|
|
2851
|
+
};
|
|
2852
|
+
for (const locale of ['en', 'vi', 'ja']) {
|
|
2853
|
+
for (const [publicSlug, documentSlug] of Object.entries(resolvedSkillDocuments)) {
|
|
2854
|
+
assert.equal(
|
|
2855
|
+
fs.existsSync(path.join(docsRoot, locale, 'skills', `${documentSlug}.mdx`)),
|
|
2856
|
+
true,
|
|
2857
|
+
`${locale} /docs/skills/${publicSlug} must resolve to ${documentSlug}.mdx`
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
for (const locale of ['en', 'vi', 'ja']) {
|
|
2863
|
+
const lifecycle = fs.readFileSync(path.join(docsRoot, locale, 'spec-lifecycle.mdx'), 'utf8');
|
|
2864
|
+
const legacyIndex = lifecycle.search(/^## Legacy compatibility$/m);
|
|
2865
|
+
assert.ok(legacyIndex >= 0, `${locale} spec lifecycle must isolate legacy compatibility`);
|
|
2866
|
+
assert.doesNotMatch(lifecycle.slice(0, legacyIndex), /\bspec\.json\b|\btask_registry\b|tasks\/task-R/);
|
|
2867
|
+
assert.match(lifecycle.slice(legacyIndex), /\bspec\.json\b/);
|
|
2868
|
+
|
|
2869
|
+
const opencode = fs.readFileSync(path.join(docsRoot, locale, 'platforms/opencode.mdx'), 'utf8');
|
|
2870
|
+
assert.match(opencode, /0\.17/);
|
|
2871
|
+
assert.match(opencode, /warning/);
|
|
2872
|
+
assert.match(opencode, /histor|lịch sử|migration/i);
|
|
2873
|
+
}
|
|
2874
|
+
});
|
|
2875
|
+
|
|
2876
|
+
const DOCS_ADAPTIVE_SOURCE_RELATIVES = {
|
|
2877
|
+
skill: 'src/claude/skills/docs/SKILL.md',
|
|
2878
|
+
init: 'src/claude/skills/docs/references/init-workflow.md',
|
|
2879
|
+
update: 'src/claude/skills/docs/references/update-workflow.md',
|
|
2880
|
+
standard: 'src/claude/skills/docs/references/standard-docs-workflow.md',
|
|
2881
|
+
};
|
|
2882
|
+
|
|
2883
|
+
function packedDocsIssues(files) {
|
|
2884
|
+
const compact = (value) => String(value).replace(/\s+/g, ' ').trim();
|
|
2885
|
+
const skill = compact(files.skill);
|
|
2886
|
+
const init = compact(files.init);
|
|
2887
|
+
const update = compact(files.update);
|
|
2888
|
+
const standard = compact(files.standard);
|
|
2889
|
+
const issues = new Set();
|
|
2890
|
+
if (!skill.includes('The user explicitly requested or permitted delegation or parallel agents.')
|
|
2891
|
+
|| !skill.includes('only through the Delegation Gate above')
|
|
2892
|
+
|| !init.includes('only through the Delegation Gate in `../SKILL.md`')
|
|
2893
|
+
|| !update.includes('only through the Delegation Gate in `../SKILL.md`')
|
|
2894
|
+
|| skill.includes('when delegation is available')
|
|
2895
|
+
|| init.includes('when delegation is available')
|
|
2896
|
+
|| update.includes('when delegation is available')) {
|
|
2897
|
+
issues.add('delegation-gate');
|
|
2898
|
+
}
|
|
2899
|
+
if (!skill.includes('## Post-Task Docs Checkpoint')
|
|
2900
|
+
|| !skill.includes('A checkpoint never invents a new document')
|
|
2901
|
+
|| !skill.includes('never auto-selects `init` and overrides')
|
|
2902
|
+
|| !standard.includes('checkpoint contract in `../SKILL.md`')
|
|
2903
|
+
|| skill.includes('checkpoint may create')) {
|
|
2904
|
+
issues.add('docs-checkpoint');
|
|
2905
|
+
}
|
|
2906
|
+
if (!skill.includes('Type: Observed | Inferred | Unknown')
|
|
2907
|
+
|| skill.includes('evidence is optional')) {
|
|
2908
|
+
issues.add('evidence-taxonomy');
|
|
2909
|
+
}
|
|
2910
|
+
if (!skill.includes('## Reconstruction Is Not Specs')
|
|
2911
|
+
|| skill.includes('are advisory')) {
|
|
2912
|
+
issues.add('reconstruction-boundary');
|
|
2913
|
+
}
|
|
2914
|
+
return [...issues].sort();
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
test('packed Claude and Codex installs reject adaptive Docs semantic weakenings', () => {
|
|
2918
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-packed-docs-adaptive-'));
|
|
2919
|
+
const destination = path.join(root, 'pack');
|
|
2920
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
2921
|
+
const canonicalBytes = new Map(Object.values(DOCS_ADAPTIVE_SOURCE_RELATIVES).map((relative) => {
|
|
2922
|
+
const sourcePath = path.join(PACKAGE_ROOT, relative);
|
|
2923
|
+
return [sourcePath, fs.readFileSync(sourcePath)];
|
|
2924
|
+
}));
|
|
2925
|
+
try {
|
|
2926
|
+
const packed = npmPack(['--pack-destination', destination, '--json'], PACKAGE_ROOT);
|
|
2927
|
+
const tarball = path.join(destination, packed.filename);
|
|
2928
|
+
const runtimeClosure = packedRuntimeClosure(path.join(root, 'runtime-closure'));
|
|
2929
|
+
assertCleanInventory(packedInventory(tarball));
|
|
2930
|
+
const layouts = {
|
|
2931
|
+
claude: { skillsRoot: '.claude/skills/docs' },
|
|
2932
|
+
codex: { skillsRoot: '.agents/skills/docs' },
|
|
2933
|
+
};
|
|
2934
|
+
const exercised = new Set();
|
|
2935
|
+
for (const [platform, layout] of Object.entries(layouts)) {
|
|
2936
|
+
const project = path.join(root, platform);
|
|
2937
|
+
const installer = installPacked(tarball, project, runtimeClosure);
|
|
2938
|
+
runInstaller(installer, project, [platform], null, ['--with-document-skills']);
|
|
2939
|
+
const readInstalled = () => ({
|
|
2940
|
+
skill: fs.readFileSync(path.join(project, layout.skillsRoot, 'SKILL.md'), 'utf8'),
|
|
2941
|
+
init: fs.readFileSync(path.join(project, layout.skillsRoot, 'references/init-workflow.md'), 'utf8'),
|
|
2942
|
+
update: fs.readFileSync(path.join(project, layout.skillsRoot, 'references/update-workflow.md'), 'utf8'),
|
|
2943
|
+
standard: fs.readFileSync(path.join(project, layout.skillsRoot, 'references/standard-docs-workflow.md'), 'utf8'),
|
|
2944
|
+
});
|
|
2945
|
+
assert.deepEqual(packedDocsIssues(readInstalled()), [], `${platform} docs baseline`);
|
|
2946
|
+
const mutations = [
|
|
2947
|
+
{ group: 'delegation-gate', file: 'SKILL.md', from: 'only through the Delegation Gate above', to: 'when delegation is available' },
|
|
2948
|
+
{ group: 'docs-checkpoint', file: 'SKILL.md', from: 'A checkpoint never invents a new document', to: 'A checkpoint may create missing documents' },
|
|
2949
|
+
{ group: 'evidence-taxonomy', file: 'SKILL.md', from: 'Type: Observed | Inferred | Unknown', to: 'Type: Observed | Unknown' },
|
|
2950
|
+
{ group: 'reconstruction-boundary', file: 'SKILL.md', from: '## Reconstruction Is Not Specs', to: '## Reconstruction Is Not Specs\n\nThe prohibitions below are advisory.' },
|
|
2951
|
+
];
|
|
2952
|
+
for (const mutation of mutations) {
|
|
2953
|
+
const target = path.join(project, layout.skillsRoot, mutation.file);
|
|
2954
|
+
const original = fs.readFileSync(target);
|
|
2955
|
+
const content = original.toString('utf8');
|
|
2956
|
+
const anchor = content.indexOf(mutation.from);
|
|
2957
|
+
assert.ok(anchor >= 0, `${platform} ${mutation.group} mutation anchor must exist`);
|
|
2958
|
+
assert.equal(
|
|
2959
|
+
content.indexOf(mutation.from, anchor + mutation.from.length),
|
|
2960
|
+
-1,
|
|
2961
|
+
`${platform} ${mutation.group} mutation anchor must be unique`
|
|
2962
|
+
);
|
|
2963
|
+
try {
|
|
2964
|
+
fs.writeFileSync(target, `${content.slice(0, anchor)}${mutation.to}${content.slice(anchor + mutation.from.length)}`);
|
|
2965
|
+
assert.deepEqual(
|
|
2966
|
+
packedDocsIssues(readInstalled()),
|
|
2967
|
+
[mutation.group],
|
|
2968
|
+
`${platform} ${mutation.group} must fail with its exact issue`
|
|
2969
|
+
);
|
|
2970
|
+
} finally {
|
|
2971
|
+
fs.writeFileSync(target, original);
|
|
2972
|
+
}
|
|
2973
|
+
assert.deepEqual(packedDocsIssues(readInstalled()), [], `${platform} ${mutation.group} restore`);
|
|
2974
|
+
for (const [sourcePath, expected] of canonicalBytes) {
|
|
2975
|
+
assert.deepEqual(fs.readFileSync(sourcePath), expected, `canonical source changed: ${sourcePath}`);
|
|
2976
|
+
}
|
|
2977
|
+
exercised.add(`${platform}:${mutation.group}`);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
assert.equal(exercised.size, 8, 'docs mutations must cover both platforms and all four groups');
|
|
2981
|
+
} finally {
|
|
2982
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
2983
|
+
}
|
|
2984
|
+
});
|
|
2985
|
+
|
|
2986
|
+
test('repository and package guides document adaptive Docs usage', () => {
|
|
2987
|
+
const guides = {
|
|
2988
|
+
repository: fs.readFileSync(path.resolve(PACKAGE_ROOT, '../../README.md'), 'utf8'),
|
|
2989
|
+
package: fs.readFileSync(path.join(PACKAGE_ROOT, 'README.md'), 'utf8'),
|
|
2990
|
+
};
|
|
2991
|
+
for (const [name, guide] of Object.entries(guides)) {
|
|
2992
|
+
assert.match(guide, /cf:docs/, `${name} guide names docs`);
|
|
2993
|
+
assert.match(guide, /post-task docs checkpoint/i, `${name} guide documents the checkpoint`);
|
|
2994
|
+
assert.match(guide, /Delegation Gate/, `${name} guide documents the gate`);
|
|
2995
|
+
assert.match(guide, /Observed \| Inferred \| Unknown/, `${name} guide documents the evidence taxonomy`);
|
|
2996
|
+
assert.doesNotMatch(guide, /docs[^\n]*\b\d+(?:\.\d+)?\s*(?:%|x faster|seconds|minutes|ms)\b/i, `${name} guide must not invent timing claims`);
|
|
2997
|
+
}
|
|
2998
|
+
});
|
|
2999
|
+
|
|
885
3000
|
test('packed Claude and Codex installs execute semantic kernel behavior without package source', () => {
|
|
886
3001
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-packed-semantic-'));
|
|
887
3002
|
const destination = path.join(root, 'pack');
|