@rpamis/comet 0.4.0-beta.5 → 0.4.0-beta.7
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 +215 -77
- package/assets/manifest.json +14 -5
- package/assets/skills/comet/SKILL.md +17 -282
- package/assets/skills/comet/reference/comet-yaml-fields.md +5 -3
- package/assets/skills/comet/reference/context-recovery.md +3 -3
- package/assets/skills/comet/reference/intent-frame.md +2 -2
- package/assets/skills/comet/reference/scripts.md +2 -2
- package/assets/skills/comet/rules/comet-workflow-guard.en.md +32 -0
- package/assets/skills/comet/rules/comet-workflow-guard.md +32 -0
- package/assets/skills/comet/scripts/comet-entry-runtime.mjs +7834 -0
- package/assets/skills/comet/scripts/comet-hook-router.mjs +10307 -0
- package/assets/skills/comet/scripts/comet-runtime.mjs +782 -425
- package/assets/skills/comet-any/SKILL.md +4 -4
- package/assets/skills/comet-any/reference/authoring-subagents.md +1 -1
- package/assets/skills/comet-any/reference/bundle-authoring.md +1 -1
- package/assets/skills/comet-any/reference/subagents/reference-author.md +1 -1
- package/assets/skills/comet-any/reference/subagents/script-author.md +1 -1
- package/assets/skills/comet-any/reference/subagents/skill-core-author.md +1 -1
- package/assets/skills/comet-any/reference/subagents/skill-reviewer.md +2 -2
- package/assets/skills/comet-any/reference/subagents/workflow-entry-author.md +3 -3
- package/assets/skills/comet-archive/SKILL.md +7 -9
- package/assets/skills/comet-build/SKILL.md +17 -11
- package/assets/skills/comet-classic/SKILL.md +293 -0
- package/assets/skills/comet-design/SKILL.md +1 -1
- package/assets/skills/comet-hotfix/SKILL.md +20 -6
- package/assets/skills/comet-native/SKILL.md +257 -0
- package/assets/skills/comet-native/reference/artifacts.md +204 -0
- package/assets/skills/comet-native/reference/commands.md +118 -0
- package/assets/skills/comet-native/reference/recovery.md +143 -0
- package/assets/skills/comet-native/scripts/comet-native-hook-guard.mjs +4 -0
- package/assets/skills/comet-native/scripts/comet-native-runtime.mjs +28898 -0
- package/assets/skills/comet-open/SKILL.md +4 -4
- package/assets/skills/comet-tweak/SKILL.md +22 -10
- package/assets/skills/comet-verify/SKILL.md +2 -0
- package/assets/skills-zh/comet/SKILL.md +17 -270
- package/assets/skills-zh/comet/reference/comet-yaml-fields.md +5 -3
- package/assets/skills-zh/comet/reference/context-recovery.md +3 -3
- package/assets/skills-zh/comet/reference/intent-frame.md +2 -2
- package/assets/skills-zh/comet/reference/scripts.md +2 -2
- package/assets/skills-zh/comet-any/SKILL.md +4 -4
- package/assets/skills-zh/comet-any/reference/authoring-subagents.md +1 -1
- package/assets/skills-zh/comet-any/reference/bundle-authoring.md +1 -1
- package/assets/skills-zh/comet-any/reference/subagents/reference-author.md +1 -1
- package/assets/skills-zh/comet-any/reference/subagents/script-author.md +1 -1
- package/assets/skills-zh/comet-any/reference/subagents/skill-core-author.md +1 -1
- package/assets/skills-zh/comet-any/reference/subagents/skill-reviewer.md +2 -2
- package/assets/skills-zh/comet-any/reference/subagents/workflow-entry-author.md +3 -3
- package/assets/skills-zh/comet-archive/SKILL.md +7 -9
- package/assets/skills-zh/comet-build/SKILL.md +17 -11
- package/assets/skills-zh/comet-classic/SKILL.md +281 -0
- package/assets/skills-zh/comet-design/SKILL.md +1 -1
- package/assets/skills-zh/comet-hotfix/SKILL.md +20 -6
- package/assets/skills-zh/comet-native/SKILL.md +257 -0
- package/assets/skills-zh/comet-native/reference/artifacts.md +206 -0
- package/assets/skills-zh/comet-native/reference/commands.md +118 -0
- package/assets/skills-zh/comet-native/reference/recovery.md +143 -0
- package/assets/skills-zh/comet-open/SKILL.md +4 -4
- package/assets/skills-zh/comet-tweak/SKILL.md +22 -10
- package/assets/skills-zh/comet-verify/SKILL.md +2 -0
- package/dist/app/cli/index.js +59 -13
- package/dist/app/cli/index.js.map +1 -1
- package/dist/app/commands/command-result.d.ts +6 -0
- package/dist/app/commands/command-result.d.ts.map +1 -0
- package/dist/app/commands/command-result.js +4 -0
- package/dist/app/commands/command-result.js.map +1 -0
- package/dist/app/commands/doctor.d.ts +1 -0
- package/dist/app/commands/doctor.d.ts.map +1 -1
- package/dist/app/commands/doctor.js +201 -26
- package/dist/app/commands/doctor.js.map +1 -1
- package/dist/app/commands/eval.d.ts +3 -1
- package/dist/app/commands/eval.d.ts.map +1 -1
- package/dist/app/commands/eval.js +17 -7
- package/dist/app/commands/eval.js.map +1 -1
- package/dist/app/commands/i18n.d.ts +1 -1
- package/dist/app/commands/i18n.d.ts.map +1 -1
- package/dist/app/commands/i18n.js +6 -2
- package/dist/app/commands/i18n.js.map +1 -1
- package/dist/app/commands/init.d.ts +10 -2
- package/dist/app/commands/init.d.ts.map +1 -1
- package/dist/app/commands/init.js +331 -50
- package/dist/app/commands/init.js.map +1 -1
- package/dist/app/commands/native.d.ts +2 -0
- package/dist/app/commands/native.d.ts.map +1 -0
- package/dist/app/commands/native.js +10 -0
- package/dist/app/commands/native.js.map +1 -0
- package/dist/app/commands/project-scope-selection.d.ts.map +1 -1
- package/dist/app/commands/project-scope-selection.js +2 -0
- package/dist/app/commands/project-scope-selection.js.map +1 -1
- package/dist/app/commands/resume-probe.d.ts.map +1 -1
- package/dist/app/commands/resume-probe.js +8 -2
- package/dist/app/commands/resume-probe.js.map +1 -1
- package/dist/app/commands/status.d.ts +0 -31
- package/dist/app/commands/status.d.ts.map +1 -1
- package/dist/app/commands/status.js +49 -172
- package/dist/app/commands/status.js.map +1 -1
- package/dist/app/commands/update.d.ts +20 -4
- package/dist/app/commands/update.d.ts.map +1 -1
- package/dist/app/commands/update.js +698 -90
- package/dist/app/commands/update.js.map +1 -1
- package/dist/app/commands/workflow.d.ts +6 -0
- package/dist/app/commands/workflow.d.ts.map +1 -0
- package/dist/app/commands/workflow.js +11 -0
- package/dist/app/commands/workflow.js.map +1 -0
- package/dist/config/repository-layout.json +21 -1
- package/dist/domains/comet-classic/classic-archive.d.ts.map +1 -1
- package/dist/domains/comet-classic/classic-archive.js +3 -0
- package/dist/domains/comet-classic/classic-archive.js.map +1 -1
- package/dist/domains/comet-classic/classic-branch-binding.d.ts +51 -0
- package/dist/domains/comet-classic/classic-branch-binding.d.ts.map +1 -0
- package/dist/domains/comet-classic/classic-branch-binding.js +107 -0
- package/dist/domains/comet-classic/classic-branch-binding.js.map +1 -0
- package/dist/domains/comet-classic/classic-current-change.d.ts +3 -5
- package/dist/domains/comet-classic/classic-current-change.d.ts.map +1 -1
- package/dist/domains/comet-classic/classic-current-change.js +59 -68
- package/dist/domains/comet-classic/classic-current-change.js.map +1 -1
- package/dist/domains/comet-classic/classic-guard.d.ts.map +1 -1
- package/dist/domains/comet-classic/classic-guard.js +40 -10
- package/dist/domains/comet-classic/classic-guard.js.map +1 -1
- package/dist/domains/comet-classic/classic-hook-guard.d.ts +9 -0
- package/dist/domains/comet-classic/classic-hook-guard.d.ts.map +1 -1
- package/dist/domains/comet-classic/classic-hook-guard.js +109 -13
- package/dist/domains/comet-classic/classic-hook-guard.js.map +1 -1
- package/dist/domains/comet-classic/classic-project-config.d.ts.map +1 -1
- package/dist/domains/comet-classic/classic-project-config.js +12 -8
- package/dist/domains/comet-classic/classic-project-config.js.map +1 -1
- package/dist/domains/comet-classic/classic-runtime-run.d.ts.map +1 -1
- package/dist/domains/comet-classic/classic-runtime-run.js +9 -1
- package/dist/domains/comet-classic/classic-runtime-run.js.map +1 -1
- package/dist/domains/comet-classic/classic-state-command.d.ts.map +1 -1
- package/dist/domains/comet-classic/classic-state-command.js +94 -4
- package/dist/domains/comet-classic/classic-state-command.js.map +1 -1
- package/dist/domains/comet-classic/classic-state-events.d.ts +1 -1
- package/dist/domains/comet-classic/classic-state-events.d.ts.map +1 -1
- package/dist/domains/comet-classic/classic-state.d.ts +2 -1
- package/dist/domains/comet-classic/classic-state.d.ts.map +1 -1
- package/dist/domains/comet-classic/classic-state.js +3 -0
- package/dist/domains/comet-classic/classic-state.js.map +1 -1
- package/dist/domains/comet-classic/classic-transitions.d.ts.map +1 -1
- package/dist/domains/comet-classic/classic-transitions.js +1 -0
- package/dist/domains/comet-classic/classic-transitions.js.map +1 -1
- package/dist/domains/comet-classic/classic-validate-command.d.ts.map +1 -1
- package/dist/domains/comet-classic/classic-validate-command.js +6 -0
- package/dist/domains/comet-classic/classic-validate-command.js.map +1 -1
- package/dist/domains/comet-entry/current-selection-repair.d.ts +17 -0
- package/dist/domains/comet-entry/current-selection-repair.d.ts.map +1 -0
- package/dist/domains/comet-entry/current-selection-repair.js +19 -0
- package/dist/domains/comet-entry/current-selection-repair.js.map +1 -0
- package/dist/domains/comet-entry/current-selection.d.ts +28 -0
- package/dist/domains/comet-entry/current-selection.d.ts.map +1 -0
- package/dist/domains/comet-entry/current-selection.js +116 -0
- package/dist/domains/comet-entry/current-selection.js.map +1 -0
- package/dist/domains/comet-entry/entry-runtime-entry.d.ts +2 -0
- package/dist/domains/comet-entry/entry-runtime-entry.d.ts.map +1 -0
- package/dist/domains/comet-entry/entry-runtime-entry.js +3 -0
- package/dist/domains/comet-entry/entry-runtime-entry.js.map +1 -0
- package/dist/domains/comet-entry/entry-runtime.d.ts +7 -0
- package/dist/domains/comet-entry/entry-runtime.d.ts.map +1 -0
- package/dist/domains/comet-entry/entry-runtime.js +55 -0
- package/dist/domains/comet-entry/entry-runtime.js.map +1 -0
- package/dist/domains/comet-entry/hook-adapter.d.ts +6 -0
- package/dist/domains/comet-entry/hook-adapter.d.ts.map +1 -0
- package/dist/domains/comet-entry/hook-adapter.js +202 -0
- package/dist/domains/comet-entry/hook-adapter.js.map +1 -0
- package/dist/domains/comet-entry/hook-router-entry.d.ts +8 -0
- package/dist/domains/comet-entry/hook-router-entry.d.ts.map +1 -0
- package/dist/domains/comet-entry/hook-router-entry.js +92 -0
- package/dist/domains/comet-entry/hook-router-entry.js.map +1 -0
- package/dist/domains/comet-entry/hook-router.d.ts +32 -0
- package/dist/domains/comet-entry/hook-router.d.ts.map +1 -0
- package/dist/domains/comet-entry/hook-router.js +133 -0
- package/dist/domains/comet-entry/hook-router.js.map +1 -0
- package/dist/domains/comet-entry/hook-types.d.ts +20 -0
- package/dist/domains/comet-entry/hook-types.d.ts.map +1 -0
- package/dist/domains/comet-entry/hook-types.js +2 -0
- package/dist/domains/comet-entry/hook-types.js.map +1 -0
- package/dist/domains/comet-entry/init-workflow.d.ts +16 -0
- package/dist/domains/comet-entry/init-workflow.d.ts.map +1 -0
- package/dist/domains/comet-entry/init-workflow.js +103 -0
- package/dist/domains/comet-entry/init-workflow.js.map +1 -0
- package/dist/domains/comet-entry/project-status.d.ts +3 -0
- package/dist/domains/comet-entry/project-status.d.ts.map +1 -0
- package/dist/domains/comet-entry/project-status.js +222 -0
- package/dist/domains/comet-entry/project-status.js.map +1 -0
- package/dist/domains/comet-entry/resolve-entry.d.ts +3 -0
- package/dist/domains/comet-entry/resolve-entry.d.ts.map +1 -0
- package/dist/domains/comet-entry/resolve-entry.js +22 -0
- package/dist/domains/comet-entry/resolve-entry.js.map +1 -0
- package/dist/domains/comet-entry/resume-probe.d.ts +34 -0
- package/dist/domains/comet-entry/resume-probe.d.ts.map +1 -0
- package/dist/domains/comet-entry/resume-probe.js +458 -0
- package/dist/domains/comet-entry/resume-probe.js.map +1 -0
- package/dist/domains/comet-entry/types.d.ts +59 -0
- package/dist/domains/comet-entry/types.d.ts.map +1 -0
- package/dist/domains/comet-entry/types.js +2 -0
- package/dist/domains/comet-entry/types.js.map +1 -0
- package/dist/domains/comet-entry/workflow-resolution.d.ts +8 -0
- package/dist/domains/comet-entry/workflow-resolution.d.ts.map +1 -0
- package/dist/domains/comet-entry/workflow-resolution.js +16 -0
- package/dist/domains/comet-entry/workflow-resolution.js.map +1 -0
- package/dist/domains/comet-native/index.d.ts +35 -0
- package/dist/domains/comet-native/index.d.ts.map +1 -0
- package/dist/domains/comet-native/index.js +35 -0
- package/dist/domains/comet-native/index.js.map +1 -0
- package/dist/domains/comet-native/native-acceptance.d.ts +42 -0
- package/dist/domains/comet-native/native-acceptance.d.ts.map +1 -0
- package/dist/domains/comet-native/native-acceptance.js +503 -0
- package/dist/domains/comet-native/native-acceptance.js.map +1 -0
- package/dist/domains/comet-native/native-archive-content.d.ts +26 -0
- package/dist/domains/comet-native/native-archive-content.d.ts.map +1 -0
- package/dist/domains/comet-native/native-archive-content.js +159 -0
- package/dist/domains/comet-native/native-archive-content.js.map +1 -0
- package/dist/domains/comet-native/native-archive-inspection.d.ts +9 -0
- package/dist/domains/comet-native/native-archive-inspection.d.ts.map +1 -0
- package/dist/domains/comet-native/native-archive-inspection.js +94 -0
- package/dist/domains/comet-native/native-archive-inspection.js.map +1 -0
- package/dist/domains/comet-native/native-archive-preflight.d.ts +52 -0
- package/dist/domains/comet-native/native-archive-preflight.d.ts.map +1 -0
- package/dist/domains/comet-native/native-archive-preflight.js +214 -0
- package/dist/domains/comet-native/native-archive-preflight.js.map +1 -0
- package/dist/domains/comet-native/native-archive-transaction.d.ts +19 -0
- package/dist/domains/comet-native/native-archive-transaction.d.ts.map +1 -0
- package/dist/domains/comet-native/native-archive-transaction.js +1181 -0
- package/dist/domains/comet-native/native-archive-transaction.js.map +1 -0
- package/dist/domains/comet-native/native-archive.d.ts +36 -0
- package/dist/domains/comet-native/native-archive.d.ts.map +1 -0
- package/dist/domains/comet-native/native-archive.js +403 -0
- package/dist/domains/comet-native/native-archive.js.map +1 -0
- package/dist/domains/comet-native/native-artifacts.d.ts +10 -0
- package/dist/domains/comet-native/native-artifacts.d.ts.map +1 -0
- package/dist/domains/comet-native/native-artifacts.js +204 -0
- package/dist/domains/comet-native/native-artifacts.js.map +1 -0
- package/dist/domains/comet-native/native-atomic-file.d.ts +10 -0
- package/dist/domains/comet-native/native-atomic-file.d.ts.map +1 -0
- package/dist/domains/comet-native/native-atomic-file.js +175 -0
- package/dist/domains/comet-native/native-atomic-file.js.map +1 -0
- package/dist/domains/comet-native/native-bounded-file.d.ts +19 -0
- package/dist/domains/comet-native/native-bounded-file.d.ts.map +1 -0
- package/dist/domains/comet-native/native-bounded-file.js +180 -0
- package/dist/domains/comet-native/native-bounded-file.js.map +1 -0
- package/dist/domains/comet-native/native-build-evidence.d.ts +42 -0
- package/dist/domains/comet-native/native-build-evidence.d.ts.map +1 -0
- package/dist/domains/comet-native/native-build-evidence.js +257 -0
- package/dist/domains/comet-native/native-build-evidence.js.map +1 -0
- package/dist/domains/comet-native/native-canonical-hash.d.ts +10 -0
- package/dist/domains/comet-native/native-canonical-hash.d.ts.map +1 -0
- package/dist/domains/comet-native/native-canonical-hash.js +100 -0
- package/dist/domains/comet-native/native-canonical-hash.js.map +1 -0
- package/dist/domains/comet-native/native-change-recovery.d.ts +11 -0
- package/dist/domains/comet-native/native-change-recovery.d.ts.map +1 -0
- package/dist/domains/comet-native/native-change-recovery.js +15 -0
- package/dist/domains/comet-native/native-change-recovery.js.map +1 -0
- package/dist/domains/comet-native/native-change.d.ts +61 -0
- package/dist/domains/comet-native/native-change.d.ts.map +1 -0
- package/dist/domains/comet-native/native-change.js +696 -0
- package/dist/domains/comet-native/native-change.js.map +1 -0
- package/dist/domains/comet-native/native-check-receipt-model.d.ts +63 -0
- package/dist/domains/comet-native/native-check-receipt-model.d.ts.map +1 -0
- package/dist/domains/comet-native/native-check-receipt-model.js +347 -0
- package/dist/domains/comet-native/native-check-receipt-model.js.map +1 -0
- package/dist/domains/comet-native/native-check-receipt-storage.d.ts +10 -0
- package/dist/domains/comet-native/native-check-receipt-storage.d.ts.map +1 -0
- package/dist/domains/comet-native/native-check-receipt-storage.js +196 -0
- package/dist/domains/comet-native/native-check-receipt-storage.js.map +1 -0
- package/dist/domains/comet-native/native-check-receipt.d.ts +19 -0
- package/dist/domains/comet-native/native-check-receipt.d.ts.map +1 -0
- package/dist/domains/comet-native/native-check-receipt.js +446 -0
- package/dist/domains/comet-native/native-check-receipt.js.map +1 -0
- package/dist/domains/comet-native/native-check.d.ts +18 -0
- package/dist/domains/comet-native/native-check.d.ts.map +1 -0
- package/dist/domains/comet-native/native-check.js +29 -0
- package/dist/domains/comet-native/native-check.js.map +1 -0
- package/dist/domains/comet-native/native-checkpoint-journal.d.ts +13 -0
- package/dist/domains/comet-native/native-checkpoint-journal.d.ts.map +1 -0
- package/dist/domains/comet-native/native-checkpoint-journal.js +53 -0
- package/dist/domains/comet-native/native-checkpoint-journal.js.map +1 -0
- package/dist/domains/comet-native/native-checkpoint-storage.d.ts +43 -0
- package/dist/domains/comet-native/native-checkpoint-storage.d.ts.map +1 -0
- package/dist/domains/comet-native/native-checkpoint-storage.js +487 -0
- package/dist/domains/comet-native/native-checkpoint-storage.js.map +1 -0
- package/dist/domains/comet-native/native-cli-entry.d.ts +2 -0
- package/dist/domains/comet-native/native-cli-entry.d.ts.map +1 -0
- package/dist/domains/comet-native/native-cli-entry.js +18 -0
- package/dist/domains/comet-native/native-cli-entry.js.map +1 -0
- package/dist/domains/comet-native/native-cli.d.ts +7 -0
- package/dist/domains/comet-native/native-cli.d.ts.map +1 -0
- package/dist/domains/comet-native/native-cli.js +642 -0
- package/dist/domains/comet-native/native-cli.js.map +1 -0
- package/dist/domains/comet-native/native-config.d.ts +15 -0
- package/dist/domains/comet-native/native-config.d.ts.map +1 -0
- package/dist/domains/comet-native/native-config.js +281 -0
- package/dist/domains/comet-native/native-config.js.map +1 -0
- package/dist/domains/comet-native/native-conflict-inspection.d.ts +17 -0
- package/dist/domains/comet-native/native-conflict-inspection.d.ts.map +1 -0
- package/dist/domains/comet-native/native-conflict-inspection.js +90 -0
- package/dist/domains/comet-native/native-conflict-inspection.js.map +1 -0
- package/dist/domains/comet-native/native-conflict-radar.d.ts +75 -0
- package/dist/domains/comet-native/native-conflict-radar.d.ts.map +1 -0
- package/dist/domains/comet-native/native-conflict-radar.js +326 -0
- package/dist/domains/comet-native/native-conflict-radar.js.map +1 -0
- package/dist/domains/comet-native/native-continuation.d.ts +9 -0
- package/dist/domains/comet-native/native-continuation.d.ts.map +1 -0
- package/dist/domains/comet-native/native-continuation.js +146 -0
- package/dist/domains/comet-native/native-continuation.js.map +1 -0
- package/dist/domains/comet-native/native-contract-files.d.ts +24 -0
- package/dist/domains/comet-native/native-contract-files.d.ts.map +1 -0
- package/dist/domains/comet-native/native-contract-files.js +66 -0
- package/dist/domains/comet-native/native-contract-files.js.map +1 -0
- package/dist/domains/comet-native/native-contract.d.ts +41 -0
- package/dist/domains/comet-native/native-contract.d.ts.map +1 -0
- package/dist/domains/comet-native/native-contract.js +141 -0
- package/dist/domains/comet-native/native-contract.js.map +1 -0
- package/dist/domains/comet-native/native-diagnostics.d.ts +18 -0
- package/dist/domains/comet-native/native-diagnostics.d.ts.map +1 -0
- package/dist/domains/comet-native/native-diagnostics.js +522 -0
- package/dist/domains/comet-native/native-diagnostics.js.map +1 -0
- package/dist/domains/comet-native/native-doctor.d.ts +12 -0
- package/dist/domains/comet-native/native-doctor.d.ts.map +1 -0
- package/dist/domains/comet-native/native-doctor.js +768 -0
- package/dist/domains/comet-native/native-doctor.js.map +1 -0
- package/dist/domains/comet-native/native-evidence-retention.d.ts +37 -0
- package/dist/domains/comet-native/native-evidence-retention.d.ts.map +1 -0
- package/dist/domains/comet-native/native-evidence-retention.js +885 -0
- package/dist/domains/comet-native/native-evidence-retention.js.map +1 -0
- package/dist/domains/comet-native/native-evidence-storage.d.ts +41 -0
- package/dist/domains/comet-native/native-evidence-storage.d.ts.map +1 -0
- package/dist/domains/comet-native/native-evidence-storage.js +351 -0
- package/dist/domains/comet-native/native-evidence-storage.js.map +1 -0
- package/dist/domains/comet-native/native-findings.d.ts +8 -0
- package/dist/domains/comet-native/native-findings.d.ts.map +1 -0
- package/dist/domains/comet-native/native-findings.js +238 -0
- package/dist/domains/comet-native/native-findings.js.map +1 -0
- package/dist/domains/comet-native/native-guards.d.ts +7 -0
- package/dist/domains/comet-native/native-guards.d.ts.map +1 -0
- package/dist/domains/comet-native/native-guards.js +126 -0
- package/dist/domains/comet-native/native-guards.js.map +1 -0
- package/dist/domains/comet-native/native-hash.d.ts +3 -0
- package/dist/domains/comet-native/native-hash.d.ts.map +1 -0
- package/dist/domains/comet-native/native-hash.js +12 -0
- package/dist/domains/comet-native/native-hash.js.map +1 -0
- package/dist/domains/comet-native/native-hook-guard.d.ts +17 -0
- package/dist/domains/comet-native/native-hook-guard.d.ts.map +1 -0
- package/dist/domains/comet-native/native-hook-guard.js +154 -0
- package/dist/domains/comet-native/native-hook-guard.js.map +1 -0
- package/dist/domains/comet-native/native-lock.d.ts +47 -0
- package/dist/domains/comet-native/native-lock.d.ts.map +1 -0
- package/dist/domains/comet-native/native-lock.js +395 -0
- package/dist/domains/comet-native/native-lock.js.map +1 -0
- package/dist/domains/comet-native/native-mutation-lock.d.ts +5 -0
- package/dist/domains/comet-native/native-mutation-lock.d.ts.map +1 -0
- package/dist/domains/comet-native/native-mutation-lock.js +66 -0
- package/dist/domains/comet-native/native-mutation-lock.js.map +1 -0
- package/dist/domains/comet-native/native-paths.d.ts +10 -0
- package/dist/domains/comet-native/native-paths.d.ts.map +1 -0
- package/dist/domains/comet-native/native-paths.js +171 -0
- package/dist/domains/comet-native/native-paths.js.map +1 -0
- package/dist/domains/comet-native/native-progress-checkpoint.d.ts +13 -0
- package/dist/domains/comet-native/native-progress-checkpoint.d.ts.map +1 -0
- package/dist/domains/comet-native/native-progress-checkpoint.js +104 -0
- package/dist/domains/comet-native/native-progress-checkpoint.js.map +1 -0
- package/dist/domains/comet-native/native-protected-file.d.ts +113 -0
- package/dist/domains/comet-native/native-protected-file.d.ts.map +1 -0
- package/dist/domains/comet-native/native-protected-file.js +509 -0
- package/dist/domains/comet-native/native-protected-file.js.map +1 -0
- package/dist/domains/comet-native/native-redaction.d.ts +2 -0
- package/dist/domains/comet-native/native-redaction.d.ts.map +1 -0
- package/dist/domains/comet-native/native-redaction.js +18 -0
- package/dist/domains/comet-native/native-redaction.js.map +1 -0
- package/dist/domains/comet-native/native-repair-integration.d.ts +42 -0
- package/dist/domains/comet-native/native-repair-integration.d.ts.map +1 -0
- package/dist/domains/comet-native/native-repair-integration.js +184 -0
- package/dist/domains/comet-native/native-repair-integration.js.map +1 -0
- package/dist/domains/comet-native/native-repair-runtime.d.ts +73 -0
- package/dist/domains/comet-native/native-repair-runtime.d.ts.map +1 -0
- package/dist/domains/comet-native/native-repair-runtime.js +509 -0
- package/dist/domains/comet-native/native-repair-runtime.js.map +1 -0
- package/dist/domains/comet-native/native-repair-stagnation.d.ts +85 -0
- package/dist/domains/comet-native/native-repair-stagnation.d.ts.map +1 -0
- package/dist/domains/comet-native/native-repair-stagnation.js +256 -0
- package/dist/domains/comet-native/native-repair-stagnation.js.map +1 -0
- package/dist/domains/comet-native/native-resume-view.d.ts +14 -0
- package/dist/domains/comet-native/native-resume-view.d.ts.map +1 -0
- package/dist/domains/comet-native/native-resume-view.js +105 -0
- package/dist/domains/comet-native/native-resume-view.js.map +1 -0
- package/dist/domains/comet-native/native-revision.d.ts +20 -0
- package/dist/domains/comet-native/native-revision.d.ts.map +1 -0
- package/dist/domains/comet-native/native-revision.js +32 -0
- package/dist/domains/comet-native/native-revision.js.map +1 -0
- package/dist/domains/comet-native/native-root-move.d.ts +20 -0
- package/dist/domains/comet-native/native-root-move.d.ts.map +1 -0
- package/dist/domains/comet-native/native-root-move.js +812 -0
- package/dist/domains/comet-native/native-root-move.js.map +1 -0
- package/dist/domains/comet-native/native-run-consistency.d.ts +3 -0
- package/dist/domains/comet-native/native-run-consistency.d.ts.map +1 -0
- package/dist/domains/comet-native/native-run-consistency.js +136 -0
- package/dist/domains/comet-native/native-run-consistency.js.map +1 -0
- package/dist/domains/comet-native/native-run-store.d.ts +41 -0
- package/dist/domains/comet-native/native-run-store.d.ts.map +1 -0
- package/dist/domains/comet-native/native-run-store.js +451 -0
- package/dist/domains/comet-native/native-run-store.js.map +1 -0
- package/dist/domains/comet-native/native-runtime-package.d.ts +16 -0
- package/dist/domains/comet-native/native-runtime-package.d.ts.map +1 -0
- package/dist/domains/comet-native/native-runtime-package.js +72 -0
- package/dist/domains/comet-native/native-runtime-package.js.map +1 -0
- package/dist/domains/comet-native/native-schema-migration.d.ts +11 -0
- package/dist/domains/comet-native/native-schema-migration.d.ts.map +1 -0
- package/dist/domains/comet-native/native-schema-migration.js +899 -0
- package/dist/domains/comet-native/native-schema-migration.js.map +1 -0
- package/dist/domains/comet-native/native-selection.d.ts +12 -0
- package/dist/domains/comet-native/native-selection.d.ts.map +1 -0
- package/dist/domains/comet-native/native-selection.js +52 -0
- package/dist/domains/comet-native/native-selection.js.map +1 -0
- package/dist/domains/comet-native/native-sensitive-paths.d.ts +7 -0
- package/dist/domains/comet-native/native-sensitive-paths.d.ts.map +1 -0
- package/dist/domains/comet-native/native-sensitive-paths.js +69 -0
- package/dist/domains/comet-native/native-sensitive-paths.js.map +1 -0
- package/dist/domains/comet-native/native-snapshot.d.ts +74 -0
- package/dist/domains/comet-native/native-snapshot.d.ts.map +1 -0
- package/dist/domains/comet-native/native-snapshot.js +2308 -0
- package/dist/domains/comet-native/native-snapshot.js.map +1 -0
- package/dist/domains/comet-native/native-specs.d.ts +12 -0
- package/dist/domains/comet-native/native-specs.d.ts.map +1 -0
- package/dist/domains/comet-native/native-specs.js +270 -0
- package/dist/domains/comet-native/native-specs.js.map +1 -0
- package/dist/domains/comet-native/native-trajectory-limits.d.ts +7 -0
- package/dist/domains/comet-native/native-trajectory-limits.d.ts.map +1 -0
- package/dist/domains/comet-native/native-trajectory-limits.js +13 -0
- package/dist/domains/comet-native/native-trajectory-limits.js.map +1 -0
- package/dist/domains/comet-native/native-trajectory-recovery.d.ts +37 -0
- package/dist/domains/comet-native/native-trajectory-recovery.d.ts.map +1 -0
- package/dist/domains/comet-native/native-trajectory-recovery.js +133 -0
- package/dist/domains/comet-native/native-trajectory-recovery.js.map +1 -0
- package/dist/domains/comet-native/native-trajectory.d.ts +16 -0
- package/dist/domains/comet-native/native-trajectory.d.ts.map +1 -0
- package/dist/domains/comet-native/native-trajectory.js +27 -0
- package/dist/domains/comet-native/native-trajectory.js.map +1 -0
- package/dist/domains/comet-native/native-transaction.d.ts +45 -0
- package/dist/domains/comet-native/native-transaction.d.ts.map +1 -0
- package/dist/domains/comet-native/native-transaction.js +846 -0
- package/dist/domains/comet-native/native-transaction.js.map +1 -0
- package/dist/domains/comet-native/native-transition-evidence.d.ts +4 -0
- package/dist/domains/comet-native/native-transition-evidence.d.ts.map +1 -0
- package/dist/domains/comet-native/native-transition-evidence.js +20 -0
- package/dist/domains/comet-native/native-transition-evidence.js.map +1 -0
- package/dist/domains/comet-native/native-transition-journal.d.ts +30 -0
- package/dist/domains/comet-native/native-transition-journal.d.ts.map +1 -0
- package/dist/domains/comet-native/native-transition-journal.js +819 -0
- package/dist/domains/comet-native/native-transition-journal.js.map +1 -0
- package/dist/domains/comet-native/native-transitions.d.ts +13 -0
- package/dist/domains/comet-native/native-transitions.d.ts.map +1 -0
- package/dist/domains/comet-native/native-transitions.js +632 -0
- package/dist/domains/comet-native/native-transitions.js.map +1 -0
- package/dist/domains/comet-native/native-types.d.ts +547 -0
- package/dist/domains/comet-native/native-types.d.ts.map +1 -0
- package/dist/domains/comet-native/native-types.js +8 -0
- package/dist/domains/comet-native/native-types.js.map +1 -0
- package/dist/domains/comet-native/native-verification-evidence.d.ts +86 -0
- package/dist/domains/comet-native/native-verification-evidence.d.ts.map +1 -0
- package/dist/domains/comet-native/native-verification-evidence.js +480 -0
- package/dist/domains/comet-native/native-verification-evidence.js.map +1 -0
- package/dist/domains/comet-native/native-verification-runtime.d.ts +58 -0
- package/dist/domains/comet-native/native-verification-runtime.d.ts.map +1 -0
- package/dist/domains/comet-native/native-verification-runtime.js +321 -0
- package/dist/domains/comet-native/native-verification-runtime.js.map +1 -0
- package/dist/domains/comet-native/native-verification-scope.d.ts +117 -0
- package/dist/domains/comet-native/native-verification-scope.d.ts.map +1 -0
- package/dist/domains/comet-native/native-verification-scope.js +1040 -0
- package/dist/domains/comet-native/native-verification-scope.js.map +1 -0
- package/dist/domains/comet-native/native-workspace.d.ts +46 -0
- package/dist/domains/comet-native/native-workspace.d.ts.map +1 -0
- package/dist/domains/comet-native/native-workspace.js +276 -0
- package/dist/domains/comet-native/native-workspace.js.map +1 -0
- package/dist/domains/dashboard/collector.d.ts.map +1 -1
- package/dist/domains/dashboard/collector.js +9 -1
- package/dist/domains/dashboard/collector.js.map +1 -1
- package/dist/domains/dashboard/native-adapter.d.ts +157 -0
- package/dist/domains/dashboard/native-adapter.d.ts.map +1 -0
- package/dist/domains/dashboard/native-adapter.js +302 -0
- package/dist/domains/dashboard/native-adapter.js.map +1 -0
- package/dist/domains/dashboard/native-collector.d.ts +6 -0
- package/dist/domains/dashboard/native-collector.d.ts.map +1 -0
- package/dist/domains/dashboard/native-collector.js +285 -0
- package/dist/domains/dashboard/native-collector.js.map +1 -0
- package/dist/domains/dashboard/types.d.ts +5 -0
- package/dist/domains/dashboard/types.d.ts.map +1 -1
- package/dist/domains/dashboard/web/assets/{arc-TGVUG2WN.js → arc-CxdiLJ50.js} +1 -1
- package/dist/domains/dashboard/web/assets/{architectureDiagram-3BPJPVTR-ASHmiHoP.js → architectureDiagram-3BPJPVTR-D5YCQu4e.js} +1 -1
- package/dist/domains/dashboard/web/assets/{blockDiagram-GPEHLZMM-CwASxBKe.js → blockDiagram-GPEHLZMM-8NfdCOQ8.js} +1 -1
- package/dist/domains/dashboard/web/assets/{c4Diagram-AAUBKEIU-BU8z_WNk.js → c4Diagram-AAUBKEIU-B3cFnNT7.js} +1 -1
- package/dist/domains/dashboard/web/assets/channel-B6AFly29.js +1 -0
- package/dist/domains/dashboard/web/assets/{chunk-2J33WTMH-CBGlUx4_.js → chunk-2J33WTMH-CBNfW-Dr.js} +1 -1
- package/dist/domains/dashboard/web/assets/{chunk-4BX2VUAB-CS-iGcFz.js → chunk-4BX2VUAB-vWyemRBr.js} +1 -1
- package/dist/domains/dashboard/web/assets/{chunk-55IACEB6-BUtCVJat.js → chunk-55IACEB6-H6haOo2C.js} +1 -1
- package/dist/domains/dashboard/web/assets/{chunk-727SXJPM-C94tj_pf.js → chunk-727SXJPM-D02UjGND.js} +1 -1
- package/dist/domains/dashboard/web/assets/{chunk-AQP2D5EJ-BFOKcpdG.js → chunk-AQP2D5EJ-wKsbOw_T.js} +1 -1
- package/dist/domains/dashboard/web/assets/{chunk-FMBD7UC4-Dmx8B-st.js → chunk-FMBD7UC4-eYFEZimP.js} +1 -1
- package/dist/domains/dashboard/web/assets/{chunk-ND2GUHAM-DYV00CNn.js → chunk-ND2GUHAM-fMaIDDI2.js} +1 -1
- package/dist/domains/dashboard/web/assets/{chunk-QZHKN3VN-DMJJyQnw.js → chunk-QZHKN3VN-C9rhF6Hm.js} +1 -1
- package/dist/domains/dashboard/web/assets/classDiagram-4FO5ZUOK-BcnrxOTt.js +1 -0
- package/dist/domains/dashboard/web/assets/classDiagram-v2-Q7XG4LA2-BcnrxOTt.js +1 -0
- package/dist/domains/dashboard/web/assets/{cose-bilkent-S5V4N54A-P4nE2rea.js → cose-bilkent-S5V4N54A-CbaShPub.js} +1 -1
- package/dist/domains/dashboard/web/assets/{cynefin-VYW2F7L2-C-R1HXDv.js → cynefin-VYW2F7L2-DN4emAQv.js} +1 -1
- package/dist/domains/dashboard/web/assets/{dagre-BM42HDAG-BH8d-C9z.js → dagre-BM42HDAG-BpSzbk_V.js} +1 -1
- package/dist/domains/dashboard/web/assets/demo-BuVau32v.js +25 -0
- package/dist/domains/dashboard/web/assets/{diagram-2AECGRRQ-DpXTgQIH.js → diagram-2AECGRRQ-CfXwGGpQ.js} +1 -1
- package/dist/domains/dashboard/web/assets/{diagram-5GNKFQAL-DSiFJzSw.js → diagram-5GNKFQAL-DMp0Zmot.js} +1 -1
- package/dist/domains/dashboard/web/assets/{diagram-KO2AKTUF-DULMkben.js → diagram-KO2AKTUF-C7ZvofsM.js} +1 -1
- package/dist/domains/dashboard/web/assets/{diagram-LMA3HP47-Pi8m_NO9.js → diagram-LMA3HP47-BjmxFnap.js} +1 -1
- package/dist/domains/dashboard/web/assets/{diagram-OG6HWLK6-BSe6bCWm.js → diagram-OG6HWLK6-DFBG1Iqy.js} +1 -1
- package/dist/domains/dashboard/web/assets/{erDiagram-TEJ5UH35-zb-FEcbq.js → erDiagram-TEJ5UH35-MpNT7dzt.js} +1 -1
- package/dist/domains/dashboard/web/assets/{flowDiagram-I6XJVG4X-Dr-Gk0z2.js → flowDiagram-I6XJVG4X-_1wAR6c1.js} +1 -1
- package/dist/domains/dashboard/web/assets/{ganttDiagram-6RSMTGT7-qs6Zb2A6.js → ganttDiagram-6RSMTGT7-DJKjDeWl.js} +1 -1
- package/dist/domains/dashboard/web/assets/{gitGraphDiagram-PVQCEYII-BQ314orT.js → gitGraphDiagram-PVQCEYII-BL7yUEn-.js} +1 -1
- package/dist/domains/dashboard/web/assets/index-CjE9mHGb.js +24 -0
- package/dist/domains/dashboard/web/assets/index-D-AGxvI4.css +1 -0
- package/dist/domains/dashboard/web/assets/{infoDiagram-5YYISTIA-O0bQp98A.js → infoDiagram-5YYISTIA-Bt5fqrOd.js} +1 -1
- package/dist/domains/dashboard/web/assets/{ishikawaDiagram-YF4QCWOH-BPC7wbKh.js → ishikawaDiagram-YF4QCWOH-COCMcjBM.js} +1 -1
- package/dist/domains/dashboard/web/assets/{journeyDiagram-JHISSGLW-ohViOd4b.js → journeyDiagram-JHISSGLW-DGcpcWny.js} +1 -1
- package/dist/domains/dashboard/web/assets/{kanban-definition-UN3LZRKU-CHleNdbG.js → kanban-definition-UN3LZRKU-QSBNsWMP.js} +1 -1
- package/dist/domains/dashboard/web/assets/{linear-CyqiW--Q.js → linear-9uyeeBZf.js} +1 -1
- package/dist/domains/dashboard/web/assets/{mermaid.core-qS9XvtaK.js → mermaid.core-CkNdB471.js} +4 -4
- package/dist/domains/dashboard/web/assets/{mindmap-definition-RKZ34NQL-oaGkqO9l.js → mindmap-definition-RKZ34NQL-CiNxI0gP.js} +1 -1
- package/dist/domains/dashboard/web/assets/{pieDiagram-4H26LBE5-CPcB-AFh.js → pieDiagram-4H26LBE5-BLJ6QnRO.js} +1 -1
- package/dist/domains/dashboard/web/assets/{quadrantDiagram-W4KKPZXB-LFy52Qtj.js → quadrantDiagram-W4KKPZXB-CflW-eFH.js} +1 -1
- package/dist/domains/dashboard/web/assets/{requirementDiagram-4Y6WPE33-BdjeaYP_.js → requirementDiagram-4Y6WPE33-B9HLmRWH.js} +1 -1
- package/dist/domains/dashboard/web/assets/{sankeyDiagram-5OEKKPKP-DDJ4styL.js → sankeyDiagram-5OEKKPKP-6xnco5k5.js} +1 -1
- package/dist/domains/dashboard/web/assets/{sequenceDiagram-3UESZ5HK-BL5bFNoz.js → sequenceDiagram-3UESZ5HK-Dp1rujNZ.js} +1 -1
- package/dist/domains/dashboard/web/assets/{stateDiagram-AJRCARHV-BNUfikui.js → stateDiagram-AJRCARHV-C1Xc90ek.js} +1 -1
- package/dist/domains/dashboard/web/assets/stateDiagram-v2-BHNVJYJU-p-qmzXyE.js +1 -0
- package/dist/domains/dashboard/web/assets/{timeline-definition-PNZ67QCA--ps2uu6L.js → timeline-definition-PNZ67QCA-g9s-m5Z4.js} +1 -1
- package/dist/domains/dashboard/web/assets/{vennDiagram-CIIHVFJN-C2q8q26c.js → vennDiagram-CIIHVFJN-rkEyhyCc.js} +1 -1
- package/dist/domains/dashboard/web/assets/{wardleyDiagram-YWT4CUSO-BUH9gXqd.js → wardleyDiagram-YWT4CUSO-CJLrWGXc.js} +1 -1
- package/dist/domains/dashboard/web/assets/{xychartDiagram-2RQKCTM6-B-DielEy.js → xychartDiagram-2RQKCTM6-BxJvPNqo.js} +1 -1
- package/dist/domains/dashboard/web/index.html +2 -2
- package/dist/domains/engine/storage-layout.d.ts +14 -0
- package/dist/domains/engine/storage-layout.d.ts.map +1 -0
- package/dist/domains/engine/storage-layout.js +32 -0
- package/dist/domains/engine/storage-layout.js.map +1 -0
- package/dist/domains/engine/storage-run.d.ts +9 -0
- package/dist/domains/engine/storage-run.d.ts.map +1 -0
- package/dist/domains/engine/storage-run.js +116 -0
- package/dist/domains/engine/storage-run.js.map +1 -0
- package/dist/domains/factory/package.d.ts.map +1 -1
- package/dist/domains/factory/package.js +5 -3
- package/dist/domains/factory/package.js.map +1 -1
- package/dist/domains/integrations/openspec.d.ts.map +1 -1
- package/dist/domains/integrations/openspec.js +8 -6
- package/dist/domains/integrations/openspec.js.map +1 -1
- package/dist/domains/skill/find.d.ts.map +1 -1
- package/dist/domains/skill/find.js +7 -1
- package/dist/domains/skill/find.js.map +1 -1
- package/dist/domains/skill/platform-inspect.d.ts +6 -2
- package/dist/domains/skill/platform-inspect.d.ts.map +1 -1
- package/dist/domains/skill/platform-inspect.js +52 -8
- package/dist/domains/skill/platform-inspect.js.map +1 -1
- package/dist/domains/skill/platform-install.d.ts +18 -7
- package/dist/domains/skill/platform-install.d.ts.map +1 -1
- package/dist/domains/skill/platform-install.js +338 -71
- package/dist/domains/skill/platform-install.js.map +1 -1
- package/dist/domains/skill/project-instructions.d.ts.map +1 -1
- package/dist/domains/skill/project-instructions.js +12 -6
- package/dist/domains/skill/project-instructions.js.map +1 -1
- package/dist/domains/skill/uninstall.d.ts.map +1 -1
- package/dist/domains/skill/uninstall.js +15 -3
- package/dist/domains/skill/uninstall.js.map +1 -1
- package/dist/domains/workflow-contract/builtins.d.ts +6 -0
- package/dist/domains/workflow-contract/builtins.d.ts.map +1 -1
- package/dist/domains/workflow-contract/builtins.js +137 -0
- package/dist/domains/workflow-contract/builtins.js.map +1 -1
- package/dist/domains/workflow-contract/index.d.ts +3 -1
- package/dist/domains/workflow-contract/index.d.ts.map +1 -1
- package/dist/domains/workflow-contract/index.js +2 -1
- package/dist/domains/workflow-contract/index.js.map +1 -1
- package/dist/domains/workflow-contract/normalize.d.ts.map +1 -1
- package/dist/domains/workflow-contract/normalize.js +28 -10
- package/dist/domains/workflow-contract/normalize.js.map +1 -1
- package/dist/domains/workflow-contract/project-config.d.ts +7 -0
- package/dist/domains/workflow-contract/project-config.d.ts.map +1 -0
- package/dist/domains/workflow-contract/project-config.js +110 -0
- package/dist/domains/workflow-contract/project-config.js.map +1 -0
- package/dist/domains/workflow-contract/types.d.ts +4 -2
- package/dist/domains/workflow-contract/types.d.ts.map +1 -1
- package/dist/domains/workflow-contract/validation.d.ts.map +1 -1
- package/dist/domains/workflow-contract/validation.js +14 -5
- package/dist/domains/workflow-contract/validation.js.map +1 -1
- package/dist/platform/install/detect.d.ts +3 -1
- package/dist/platform/install/detect.d.ts.map +1 -1
- package/dist/platform/install/detect.js +4 -1
- package/dist/platform/install/detect.js.map +1 -1
- package/dist/platform/install/project-registry.js +2 -2
- package/dist/platform/install/project-registry.js.map +1 -1
- package/dist/platform/paths/repository-layout.d.ts +16 -0
- package/dist/platform/paths/repository-layout.d.ts.map +1 -1
- package/dist/platform/paths/repository-layout.js.map +1 -1
- package/eval/local/README.md +50 -0
- package/eval/local/scripts/compare_baselines.py +127 -1
- package/eval/local/scripts/regression_check.py +3 -4
- package/eval/local/tasks/comet-native-clarification/environment/Dockerfile +27 -0
- package/eval/local/tasks/comet-native-clarification/environment/test_wordcount.py +25 -0
- package/eval/local/tasks/comet-native-clarification/environment/wordcount.py +36 -0
- package/eval/local/tasks/comet-native-clarification/instruction.md +7 -0
- package/eval/local/tasks/comet-native-clarification/task.toml +40 -0
- package/eval/local/tasks/comet-native-clarification/validation/test_native_clarification.py +193 -0
- package/eval/local/tasks/comet-native-clarification-modes/environment/Dockerfile +27 -0
- package/eval/local/tasks/comet-native-clarification-modes/environment/test_wordcount.py +25 -0
- package/eval/local/tasks/comet-native-clarification-modes/environment/wordcount.py +36 -0
- package/eval/local/tasks/comet-native-clarification-modes/instruction.md +13 -0
- package/eval/local/tasks/comet-native-clarification-modes/task.toml +39 -0
- package/eval/local/tasks/comet-native-clarification-modes/validation/test_native_clarification_modes.py +235 -0
- package/eval/local/tasks/comet-native-interrupted-transition/environment/.comet/config.yaml +6 -0
- package/eval/local/tasks/comet-native-interrupted-transition/environment/Dockerfile +28 -0
- package/eval/local/tasks/comet-native-interrupted-transition/environment/docs/comet/changes/add-character-counting/brief.md +17 -0
- package/eval/local/tasks/comet-native-interrupted-transition/environment/docs/comet/changes/add-character-counting/comet-state.yaml +12 -0
- package/eval/local/tasks/comet-native-interrupted-transition/environment/docs/comet/changes/add-character-counting/runtime/transition.json +69 -0
- package/eval/local/tasks/comet-native-interrupted-transition/environment/docs/comet/changes/add-character-counting/specs/character-counting/spec.md +3 -0
- package/eval/local/tasks/comet-native-interrupted-transition/environment/test_wordcount.py +20 -0
- package/eval/local/tasks/comet-native-interrupted-transition/environment/wordcount.py +27 -0
- package/eval/local/tasks/comet-native-interrupted-transition/instruction.md +7 -0
- package/eval/local/tasks/comet-native-interrupted-transition/task.toml +35 -0
- package/eval/local/tasks/comet-native-interrupted-transition/validation/test_native_interrupted_transition.py +63 -0
- package/eval/local/tasks/comet-native-repository-fact/environment/Dockerfile +26 -0
- package/eval/local/tasks/comet-native-repository-fact/environment/PRODUCT_RULES.md +3 -0
- package/eval/local/tasks/comet-native-repository-fact/environment/test_wordcount.py +20 -0
- package/eval/local/tasks/comet-native-repository-fact/environment/wordcount.py +27 -0
- package/eval/local/tasks/comet-native-repository-fact/instruction.md +7 -0
- package/eval/local/tasks/comet-native-repository-fact/task.toml +35 -0
- package/eval/local/tasks/comet-native-repository-fact/validation/test_native_repository_fact.py +53 -0
- package/eval/local/tasks/comet-native-wave-b-decision-resume/environment/Dockerfile +27 -0
- package/eval/local/tasks/comet-native-wave-b-decision-resume/environment/test_wordcount.py +27 -0
- package/eval/local/tasks/comet-native-wave-b-decision-resume/environment/wordcount.py +36 -0
- package/eval/local/tasks/comet-native-wave-b-decision-resume/instruction.md +9 -0
- package/eval/local/tasks/comet-native-wave-b-decision-resume/task.toml +40 -0
- package/eval/local/tasks/comet-native-wave-b-decision-resume/validation/test_native_wave_b_decision_resume.py +208 -0
- package/eval/local/tasks/comet-native-wave-c-verification-integrity/environment/Dockerfile +27 -0
- package/eval/local/tasks/comet-native-wave-c-verification-integrity/environment/test_wordcount.py +27 -0
- package/eval/local/tasks/comet-native-wave-c-verification-integrity/environment/wordcount.py +36 -0
- package/eval/local/tasks/comet-native-wave-c-verification-integrity/instruction.md +11 -0
- package/eval/local/tasks/comet-native-wave-c-verification-integrity/task.toml +36 -0
- package/eval/local/tasks/comet-native-wave-c-verification-integrity/validation/test_native_wave_c_verification_integrity.py +344 -0
- package/eval/local/tasks/comet-native-wave-d-stagnation-stop/environment/Dockerfile +27 -0
- package/eval/local/tasks/comet-native-wave-d-stagnation-stop/environment/test_wordcount.py +27 -0
- package/eval/local/tasks/comet-native-wave-d-stagnation-stop/environment/wordcount.py +36 -0
- package/eval/local/tasks/comet-native-wave-d-stagnation-stop/instruction.md +11 -0
- package/eval/local/tasks/comet-native-wave-d-stagnation-stop/task.toml +37 -0
- package/eval/local/tasks/comet-native-wave-d-stagnation-stop/validation/test_native_wave_d_stagnation_stop.py +284 -0
- package/eval/local/tasks/comet-native-wave-e-parallel-safety/environment/.include-trusted-native-runtime +1 -0
- package/eval/local/tasks/comet-native-wave-e-parallel-safety/environment/Dockerfile +27 -0
- package/eval/local/tasks/comet-native-wave-e-parallel-safety/environment/test_wordcount.py +27 -0
- package/eval/local/tasks/comet-native-wave-e-parallel-safety/environment/wordcount.py +36 -0
- package/eval/local/tasks/comet-native-wave-e-parallel-safety/instruction.md +9 -0
- package/eval/local/tasks/comet-native-wave-e-parallel-safety/task.toml +37 -0
- package/eval/local/tasks/comet-native-wave-e-parallel-safety/validation/test_native_wave_e_parallel_safety.py +309 -0
- package/eval/local/tasks/comet-native-wave-f-dashboard-readonly/environment/.include-current-comet-cli +1 -0
- package/eval/local/tasks/comet-native-wave-f-dashboard-readonly/environment/Dockerfile +31 -0
- package/eval/local/tasks/comet-native-wave-f-dashboard-readonly/environment/current-comet-package.json +18 -0
- package/eval/local/tasks/comet-native-wave-f-dashboard-readonly/environment/current-comet.sh +14 -0
- package/eval/local/tasks/comet-native-wave-f-dashboard-readonly/environment/test_wordcount.py +27 -0
- package/eval/local/tasks/comet-native-wave-f-dashboard-readonly/environment/wordcount.py +36 -0
- package/eval/local/tasks/comet-native-wave-f-dashboard-readonly/instruction.md +7 -0
- package/eval/local/tasks/comet-native-wave-f-dashboard-readonly/task.toml +36 -0
- package/eval/local/tasks/comet-native-wave-f-dashboard-readonly/validation/test_native_wave_f_dashboard_readonly.py +469 -0
- package/eval/local/tasks/comet-native-workflow/environment/Dockerfile +27 -0
- package/eval/local/tasks/comet-native-workflow/environment/test_wordcount.py +25 -0
- package/eval/local/tasks/comet-native-workflow/environment/wordcount.py +36 -0
- package/eval/local/tasks/comet-native-workflow/instruction.md +14 -0
- package/eval/local/tasks/comet-native-workflow/task.toml +36 -0
- package/eval/local/tasks/comet-native-workflow/validation/test_native_workflow.py +149 -0
- package/eval/local/tasks/index.yaml +61 -0
- package/eval/local/tests/conftest.py +407 -22
- package/eval/local/tests/scaffold/test_aligned_comparison.py +1012 -0
- package/eval/local/tests/scaffold/test_conftest_helpers.py +187 -7
- package/eval/local/tests/scaffold/test_logging.py +135 -1
- package/eval/local/tests/scaffold/test_manifests.py +2 -0
- package/eval/local/tests/scaffold/test_native_aligned_eval.py +346 -0
- package/eval/local/tests/scaffold/test_native_wave_evaluations.py +1889 -0
- package/eval/local/tests/scaffold/test_profiles.py +33 -0
- package/eval/local/tests/scaffold/test_sample_quality.py +57 -0
- package/eval/local/tests/scaffold/test_tasks.py +377 -1
- package/eval/local/tests/scaffold/test_treatments.py +59 -13
- package/eval/local/tests/scaffold/test_utils.py +277 -4
- package/eval/local/tests/tasks/__init__.py +1 -0
- package/eval/local/tests/tasks/test_tasks.py +104 -78
- package/eval/local/tests/tasks/test_validation_scripts.py +37 -0
- package/eval/local/treatments/comet/comet_native_phase1.yaml +66 -0
- package/eval/scaffold/python/aligned_comparison.py +1631 -0
- package/eval/scaffold/python/generic_llm_judge.py +3 -1
- package/eval/scaffold/python/llm_judge.py +0 -3
- package/eval/scaffold/python/logging.py +115 -16
- package/eval/scaffold/python/manifests.py +2 -0
- package/eval/scaffold/python/native_eval.py +138 -0
- package/eval/scaffold/python/pass_at_k.py +0 -1
- package/eval/scaffold/python/report_outputs.py +226 -44
- package/eval/scaffold/python/sample_quality.py +45 -2
- package/eval/scaffold/python/tasks.py +11 -0
- package/eval/scaffold/python/utils.py +108 -20
- package/eval/scaffold/python/validation/__init__.py +15 -5
- package/eval/scaffold/python/validation/comet_workflow.py +10 -5
- package/eval/scaffold/python/validation/generic_rubric.py +1 -2
- package/eval/scaffold/python/validation/native_wave.py +2154 -0
- package/eval/scaffold/python/validation/native_workflow.py +252 -0
- package/eval/scaffold/python/validation/rubric.py +0 -2
- package/eval/scaffold/shell/completion-point.sh +18 -0
- package/eval/scaffold/shell/decision-point.sh +35 -0
- package/eval/scaffold/shell/docker.sh +150 -9
- package/eval/scaffold/shell/run-claude-loop.sh +57 -27
- package/package.json +19 -5
- package/dist/domains/dashboard/web/assets/channel-kp6Ng2JJ.js +0 -1
- package/dist/domains/dashboard/web/assets/classDiagram-4FO5ZUOK-yBDjgMY_.js +0 -1
- package/dist/domains/dashboard/web/assets/classDiagram-v2-Q7XG4LA2-yBDjgMY_.js +0 -1
- package/dist/domains/dashboard/web/assets/demo-DQyfozbi.js +0 -1
- package/dist/domains/dashboard/web/assets/index-BLAfkNTu.css +0 -1
- package/dist/domains/dashboard/web/assets/index-DtLUOcAW.js +0 -24
- package/dist/domains/dashboard/web/assets/stateDiagram-v2-BHNVJYJU-DGrsmWts.js +0 -1
- package/eval/.env.example +0 -52
- package/eval/langsmith/.env.example +0 -16
|
@@ -0,0 +1,2154 @@
|
|
|
1
|
+
"""Shared hard checks for the Comet Native wave B-F evaluations.
|
|
2
|
+
|
|
3
|
+
The wave tasks intentionally exercise different runtime contracts, but their
|
|
4
|
+
evidence is all machine-readable JSON. Keeping the parsing here prevents each
|
|
5
|
+
task validator from inventing a slightly different interpretation of Native
|
|
6
|
+
state, compare-and-swap outcomes, or Dashboard projections.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import hashlib
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import unicodedata
|
|
18
|
+
from collections.abc import Iterable
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from pathlib import PurePosixPath
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
HASH_PATTERN = re.compile(r"^[a-f0-9]{64}$")
|
|
26
|
+
ACCEPTANCE_ID_PATTERN = re.compile(r"^acceptance-[a-f0-9]{64}$")
|
|
27
|
+
SCOPE_ID_PATTERN = re.compile(r"^scope:[a-f0-9]{64}$")
|
|
28
|
+
CHANGE_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$")
|
|
29
|
+
MAX_EVIDENCE_BYTES = 1024 * 1024
|
|
30
|
+
MAX_SAFE_INTEGER = 2**53 - 1
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class NativeEvidenceError(ValueError):
|
|
34
|
+
"""Raised when an eval artifact is not an exact Native runtime document."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def canonical_json(value: Any) -> str:
|
|
38
|
+
"""Mirror Native's canonical JSON for JSON-domain eval documents."""
|
|
39
|
+
try:
|
|
40
|
+
return json.dumps(
|
|
41
|
+
value,
|
|
42
|
+
ensure_ascii=False,
|
|
43
|
+
allow_nan=False,
|
|
44
|
+
separators=(",", ":"),
|
|
45
|
+
sort_keys=True,
|
|
46
|
+
)
|
|
47
|
+
except (TypeError, ValueError) as error:
|
|
48
|
+
raise NativeEvidenceError(f"Value is not canonical JSON: {error}") from error
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def canonical_hash(tag: str, value: Any) -> str:
|
|
52
|
+
if not tag or "\n" in tag or "\r" in tag:
|
|
53
|
+
raise NativeEvidenceError("Canonical hash tag is invalid")
|
|
54
|
+
return hashlib.sha256(f"{tag}\n{canonical_json(value)}".encode()).hexdigest()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
NATIVE_CHECK_LIMITS = {
|
|
58
|
+
"maxFiles": 256,
|
|
59
|
+
"maxFileBytes": 1024 * 1024,
|
|
60
|
+
"maxTotalBytes": 8 * 1024 * 1024,
|
|
61
|
+
"maxIssues": 128,
|
|
62
|
+
}
|
|
63
|
+
NATIVE_CHECKER_HASH = canonical_hash(
|
|
64
|
+
"comet.native.checker-policy.v1",
|
|
65
|
+
{
|
|
66
|
+
"policy": "scoped-text-safety",
|
|
67
|
+
"version": 1,
|
|
68
|
+
"limits": NATIVE_CHECK_LIMITS,
|
|
69
|
+
"checks": ["conflict-marker", "space-before-tab", "trailing-whitespace"],
|
|
70
|
+
"binaryHandling": "skip-and-count",
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
NATIVE_CHECK_ISSUE_KINDS = (
|
|
74
|
+
"conflict-marker",
|
|
75
|
+
"trailing-whitespace",
|
|
76
|
+
"space-before-tab",
|
|
77
|
+
"scope-mismatch",
|
|
78
|
+
"unsafe-file",
|
|
79
|
+
"scan-limit",
|
|
80
|
+
)
|
|
81
|
+
NATIVE_CHECK_STALE_REASONS = (
|
|
82
|
+
"contract-before-does-not-match-scope",
|
|
83
|
+
"implementation-before-does-not-match-scope",
|
|
84
|
+
"contract-changed-during-check",
|
|
85
|
+
"implementation-changed-during-check",
|
|
86
|
+
"contract-after-does-not-match-scope",
|
|
87
|
+
"implementation-after-does-not-match-scope",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _record(value: Any, label: str) -> dict[str, Any]:
|
|
92
|
+
if not isinstance(value, dict):
|
|
93
|
+
raise NativeEvidenceError(f"{label} must be an object")
|
|
94
|
+
return value
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _exact_keys(
|
|
98
|
+
value: dict[str, Any],
|
|
99
|
+
required: Iterable[str],
|
|
100
|
+
optional: Iterable[str] = (),
|
|
101
|
+
*,
|
|
102
|
+
label: str,
|
|
103
|
+
) -> None:
|
|
104
|
+
required_set = set(required)
|
|
105
|
+
allowed = required_set | set(optional)
|
|
106
|
+
missing = sorted(required_set - set(value))
|
|
107
|
+
unknown = sorted(set(value) - allowed)
|
|
108
|
+
if missing or unknown:
|
|
109
|
+
raise NativeEvidenceError(
|
|
110
|
+
f"{label} fields are invalid: missing={missing}, unknown={unknown}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _hash(value: Any, label: str) -> str:
|
|
115
|
+
if not isinstance(value, str) or not HASH_PATTERN.fullmatch(value):
|
|
116
|
+
raise NativeEvidenceError(f"{label} must be a SHA-256 hash")
|
|
117
|
+
return value
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _positive_int(value: Any, label: str) -> int:
|
|
121
|
+
if (
|
|
122
|
+
isinstance(value, bool)
|
|
123
|
+
or not isinstance(value, int)
|
|
124
|
+
or value < 1
|
|
125
|
+
or value > MAX_SAFE_INTEGER
|
|
126
|
+
):
|
|
127
|
+
raise NativeEvidenceError(f"{label} must be a positive integer")
|
|
128
|
+
return value
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _nonnegative_int(value: Any, label: str) -> int:
|
|
132
|
+
if (
|
|
133
|
+
isinstance(value, bool)
|
|
134
|
+
or not isinstance(value, int)
|
|
135
|
+
or value < 0
|
|
136
|
+
or value > MAX_SAFE_INTEGER
|
|
137
|
+
):
|
|
138
|
+
raise NativeEvidenceError(f"{label} must be a non-negative integer")
|
|
139
|
+
return value
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _timestamp(value: Any, label: str) -> str:
|
|
143
|
+
if not isinstance(value, str) or not re.fullmatch(
|
|
144
|
+
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z", value
|
|
145
|
+
):
|
|
146
|
+
raise NativeEvidenceError(f"{label} must be a canonical ISO timestamp")
|
|
147
|
+
try:
|
|
148
|
+
parsed = datetime.fromisoformat(value.removesuffix("Z") + "+00:00")
|
|
149
|
+
except ValueError as error:
|
|
150
|
+
raise NativeEvidenceError(f"{label} must be a canonical ISO timestamp") from error
|
|
151
|
+
if parsed.isoformat(timespec="milliseconds").replace("+00:00", "Z") != value:
|
|
152
|
+
raise NativeEvidenceError(f"{label} must be a canonical ISO timestamp")
|
|
153
|
+
return value
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def portable_ref(value: Any, label: str) -> str:
|
|
157
|
+
if not isinstance(value, str) or not value or value != value.strip():
|
|
158
|
+
raise NativeEvidenceError(f"{label} must be a non-empty relative ref")
|
|
159
|
+
if "\\" in value or any(
|
|
160
|
+
ord(character) <= 0x1F or ord(character) == 0x7F for character in value
|
|
161
|
+
):
|
|
162
|
+
raise NativeEvidenceError(f"{label} must be portable")
|
|
163
|
+
ref = PurePosixPath(value)
|
|
164
|
+
if ref.is_absolute() or ".." in ref.parts or "." in ref.parts or str(ref) != value:
|
|
165
|
+
raise NativeEvidenceError(f"{label} must be a normalized relative ref")
|
|
166
|
+
if re.match(r"^(?:[A-Za-z]:|~)", value) or value.endswith("/"):
|
|
167
|
+
raise NativeEvidenceError(f"{label} must be a normalized relative ref")
|
|
168
|
+
return value
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _assert_real_directory_chain(root: Path, target: Path, label: str) -> None:
|
|
172
|
+
root = root.absolute()
|
|
173
|
+
target = target.absolute()
|
|
174
|
+
try:
|
|
175
|
+
relative = target.relative_to(root)
|
|
176
|
+
except ValueError as error:
|
|
177
|
+
raise NativeEvidenceError(f"{label} escapes its root") from error
|
|
178
|
+
cursor = root
|
|
179
|
+
if cursor.is_symlink() or not cursor.is_dir():
|
|
180
|
+
raise NativeEvidenceError(f"{label} root must be a real directory")
|
|
181
|
+
for part in relative.parts:
|
|
182
|
+
cursor = cursor / part
|
|
183
|
+
if cursor.is_symlink():
|
|
184
|
+
raise NativeEvidenceError(f"{label} crosses a symbolic link: {cursor}")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def contained_file(root: Path, ref: Any, label: str) -> Path:
|
|
188
|
+
reference = portable_ref(ref, label)
|
|
189
|
+
target = root.joinpath(*PurePosixPath(reference).parts)
|
|
190
|
+
_assert_real_directory_chain(root, target.parent, label)
|
|
191
|
+
if target.is_symlink() or not target.is_file():
|
|
192
|
+
raise NativeEvidenceError(f"{label} is not a real regular file: {reference}")
|
|
193
|
+
try:
|
|
194
|
+
target.resolve(strict=True).relative_to(root.resolve(strict=True))
|
|
195
|
+
except (OSError, ValueError) as error:
|
|
196
|
+
raise NativeEvidenceError(f"{label} resolves outside its root") from error
|
|
197
|
+
return target
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def read_contained_json(root: Path, ref: Any, label: str) -> Any:
|
|
201
|
+
target = contained_file(root, ref, label)
|
|
202
|
+
if target.stat().st_size > MAX_EVIDENCE_BYTES:
|
|
203
|
+
raise NativeEvidenceError(f"{label} exceeds its byte budget")
|
|
204
|
+
try:
|
|
205
|
+
return json.loads(target.read_text(encoding="utf-8"))
|
|
206
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
207
|
+
raise NativeEvidenceError(f"{label} is invalid JSON: {error}") from error
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def sha256_file(path: Path) -> str:
|
|
211
|
+
digest = hashlib.sha256()
|
|
212
|
+
with path.open("rb") as handle:
|
|
213
|
+
for chunk in iter(lambda: handle.read(64 * 1024), b""):
|
|
214
|
+
digest.update(chunk)
|
|
215
|
+
return digest.hexdigest()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def passed(check: str, message: str = "") -> dict[str, str]:
|
|
219
|
+
result = {"check": check, "status": "passed"}
|
|
220
|
+
if message:
|
|
221
|
+
result["message"] = message
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def failed(check: str, reason: str) -> dict[str, str]:
|
|
226
|
+
return {"check": check, "status": "failed", "reason": reason}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def read_json(path: Path) -> Any:
|
|
230
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def parse_runtime_envelope(
|
|
234
|
+
path: Path,
|
|
235
|
+
*,
|
|
236
|
+
command: str | None = None,
|
|
237
|
+
exit_code: int | None = None,
|
|
238
|
+
) -> dict[str, Any]:
|
|
239
|
+
if path.is_symlink() or not path.is_file():
|
|
240
|
+
raise NativeEvidenceError(f"Missing real runtime JSON evidence: {path}")
|
|
241
|
+
if path.stat().st_size > MAX_EVIDENCE_BYTES:
|
|
242
|
+
raise NativeEvidenceError(f"Runtime envelope exceeds its byte budget: {path.name}")
|
|
243
|
+
try:
|
|
244
|
+
payload = read_json(path)
|
|
245
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
246
|
+
raise NativeEvidenceError(f"Invalid runtime JSON in {path.name}: {error}") from error
|
|
247
|
+
envelope = _record(payload, f"Runtime envelope {path.name}")
|
|
248
|
+
_exact_keys(
|
|
249
|
+
envelope,
|
|
250
|
+
["command", "exitCode", "data"],
|
|
251
|
+
["error"],
|
|
252
|
+
label=f"Runtime envelope {path.name}",
|
|
253
|
+
)
|
|
254
|
+
if not isinstance(envelope["command"], str) or not envelope["command"]:
|
|
255
|
+
raise NativeEvidenceError(f"{path.name} has no exact command")
|
|
256
|
+
if (
|
|
257
|
+
isinstance(envelope["exitCode"], bool)
|
|
258
|
+
or not isinstance(envelope["exitCode"], int)
|
|
259
|
+
or not isinstance(envelope["data"], dict)
|
|
260
|
+
):
|
|
261
|
+
raise NativeEvidenceError(f"{path.name} has an invalid exitCode/data envelope")
|
|
262
|
+
if "error" in envelope:
|
|
263
|
+
error = _record(envelope["error"], f"Runtime error {path.name}")
|
|
264
|
+
_exact_keys(error, ["code", "message"], label=f"Runtime error {path.name}")
|
|
265
|
+
if error["code"] not in {"usage", "invalid-data", "blocked", "conflict", "internal"}:
|
|
266
|
+
raise NativeEvidenceError(f"{path.name} has an invalid runtime error code")
|
|
267
|
+
if not isinstance(error["message"], str) or not error["message"]:
|
|
268
|
+
raise NativeEvidenceError(f"{path.name} has an invalid runtime error message")
|
|
269
|
+
if command is not None and envelope["command"] != command:
|
|
270
|
+
raise NativeEvidenceError(
|
|
271
|
+
f"{path.name} expected command {command!r}; observed {envelope['command']!r}"
|
|
272
|
+
)
|
|
273
|
+
if exit_code is not None and envelope["exitCode"] != exit_code:
|
|
274
|
+
raise NativeEvidenceError(
|
|
275
|
+
f"{path.name} expected exit {exit_code}; observed {envelope['exitCode']!r}"
|
|
276
|
+
)
|
|
277
|
+
return envelope
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _walk(value: Any) -> Iterable[Any]:
|
|
281
|
+
yield value
|
|
282
|
+
if isinstance(value, dict):
|
|
283
|
+
for child in value.values():
|
|
284
|
+
yield from _walk(child)
|
|
285
|
+
elif isinstance(value, list):
|
|
286
|
+
for child in value:
|
|
287
|
+
yield from _walk(child)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _normalise_token(value: Any) -> str:
|
|
291
|
+
return str(value).strip().lower().replace("_", "-").replace(" ", "-")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def check_json_state(path: Path, expected_state: str) -> dict[str, str]:
|
|
295
|
+
"""Require an exact state token anywhere in a JSON evidence payload."""
|
|
296
|
+
check = f"json_state_{expected_state}"
|
|
297
|
+
if not path.is_file():
|
|
298
|
+
return failed(check, f"Missing evidence file: {path}")
|
|
299
|
+
try:
|
|
300
|
+
payload = read_json(path)
|
|
301
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
302
|
+
return failed(check, f"Invalid JSON in {path.name}: {error}")
|
|
303
|
+
|
|
304
|
+
expected = _normalise_token(expected_state)
|
|
305
|
+
tokens = {
|
|
306
|
+
_normalise_token(value)
|
|
307
|
+
for value in _walk(payload)
|
|
308
|
+
if isinstance(value, (str, int, float, bool))
|
|
309
|
+
}
|
|
310
|
+
if expected not in tokens:
|
|
311
|
+
return failed(check, f"Expected state {expected_state!r}; observed {sorted(tokens)}")
|
|
312
|
+
return passed(check)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def check_runtime_envelopes(paths: Iterable[Path]) -> dict[str, str]:
|
|
316
|
+
"""Require exact Native CLI JSON envelopes instead of prose reconstructions."""
|
|
317
|
+
check = "runtime_json_envelopes"
|
|
318
|
+
for path in paths:
|
|
319
|
+
try:
|
|
320
|
+
parse_runtime_envelope(path)
|
|
321
|
+
except NativeEvidenceError as error:
|
|
322
|
+
return failed(check, str(error))
|
|
323
|
+
return passed(check)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _normalize_acceptance_text(value: str) -> str:
|
|
327
|
+
return re.sub(r"\s+", " ", unicodedata.normalize("NFC", value)).strip()
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _heading(line: str) -> tuple[int, str] | None:
|
|
331
|
+
match = re.fullmatch(r" {0,3}(#{1,6})[ \t]+(.+?)[ \t]*", line)
|
|
332
|
+
if not match:
|
|
333
|
+
return None
|
|
334
|
+
text = re.sub(r"[ \t]+#+[ \t]*$", "", match.group(2)).strip()
|
|
335
|
+
return len(match.group(1)), text
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _scanned_markdown(markdown: str) -> list[tuple[str, bool]]:
|
|
339
|
+
scanned: list[tuple[str, bool]] = []
|
|
340
|
+
fence: tuple[str, int] | None = None
|
|
341
|
+
html_comment = False
|
|
342
|
+
html_tag: str | None = None
|
|
343
|
+
for line in markdown.replace("\r\n", "\n").replace("\r", "\n").split("\n"):
|
|
344
|
+
scanned.append((line, fence is None and not html_comment and html_tag is None))
|
|
345
|
+
fence_match = re.match(r"^ {0,3}(`{3,}|~{3,})(.*)$", line)
|
|
346
|
+
if fence is not None:
|
|
347
|
+
if (
|
|
348
|
+
fence_match
|
|
349
|
+
and fence_match.group(1)[0] == fence[0]
|
|
350
|
+
and len(fence_match.group(1)) >= fence[1]
|
|
351
|
+
and not fence_match.group(2).strip()
|
|
352
|
+
):
|
|
353
|
+
fence = None
|
|
354
|
+
continue
|
|
355
|
+
if html_comment:
|
|
356
|
+
if "-->" in line:
|
|
357
|
+
html_comment = False
|
|
358
|
+
continue
|
|
359
|
+
if html_tag is not None:
|
|
360
|
+
if re.search(rf"</{re.escape(html_tag)}\s*>", line, re.IGNORECASE):
|
|
361
|
+
html_tag = None
|
|
362
|
+
continue
|
|
363
|
+
if fence_match:
|
|
364
|
+
fence = (fence_match.group(1)[0], len(fence_match.group(1)))
|
|
365
|
+
continue
|
|
366
|
+
trimmed = line.lstrip()
|
|
367
|
+
if trimmed.startswith("<!--") and "-->" not in trimmed:
|
|
368
|
+
html_comment = True
|
|
369
|
+
continue
|
|
370
|
+
html_start = re.match(r"<([A-Za-z][A-Za-z0-9-]*)\b[^>]*>", trimmed)
|
|
371
|
+
if (
|
|
372
|
+
html_start
|
|
373
|
+
and not trimmed.startswith("</")
|
|
374
|
+
and not trimmed.endswith("/>")
|
|
375
|
+
and not re.search(rf"</{re.escape(html_start.group(1))}\s*>", trimmed, re.IGNORECASE)
|
|
376
|
+
):
|
|
377
|
+
html_tag = html_start.group(1)
|
|
378
|
+
return scanned
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _criterion(kind: str, source: str, text: str, context: list[str] | None = None) -> dict:
|
|
382
|
+
normalized = {
|
|
383
|
+
"kind": kind,
|
|
384
|
+
"source": portable_ref(source, "Acceptance source"),
|
|
385
|
+
"context": [_normalize_acceptance_text(item) for item in (context or [])],
|
|
386
|
+
"text": _normalize_acceptance_text(text),
|
|
387
|
+
}
|
|
388
|
+
if not normalized["text"]:
|
|
389
|
+
raise NativeEvidenceError("Acceptance criterion text is empty")
|
|
390
|
+
return {
|
|
391
|
+
"id": f"acceptance-{canonical_hash('comet.native.acceptance.v1', normalized)}",
|
|
392
|
+
**normalized,
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _brief_acceptance(markdown: str, source: str) -> list[dict]:
|
|
397
|
+
lines = _scanned_markdown(markdown)
|
|
398
|
+
starts = [
|
|
399
|
+
index
|
|
400
|
+
for index, (line, body) in enumerate(lines)
|
|
401
|
+
if body
|
|
402
|
+
and (candidate := _heading(line)) is not None
|
|
403
|
+
and candidate[0] == 1
|
|
404
|
+
and candidate[1].lower() == "acceptance examples"
|
|
405
|
+
]
|
|
406
|
+
if not starts:
|
|
407
|
+
return []
|
|
408
|
+
if len(starts) != 1:
|
|
409
|
+
raise NativeEvidenceError("Brief must contain exactly one Acceptance examples section")
|
|
410
|
+
start = starts[0] + 1
|
|
411
|
+
end = len(lines)
|
|
412
|
+
for index in range(start, len(lines)):
|
|
413
|
+
heading = _heading(lines[index][0]) if lines[index][1] else None
|
|
414
|
+
if heading and heading[0] == 1:
|
|
415
|
+
end = index
|
|
416
|
+
break
|
|
417
|
+
section = lines[start:end]
|
|
418
|
+
indents = [
|
|
419
|
+
len(match.group(1))
|
|
420
|
+
for line, body in section
|
|
421
|
+
if body and (match := re.match(r"^( {0,3})[-*+][ \t]+", line))
|
|
422
|
+
]
|
|
423
|
+
if not indents:
|
|
424
|
+
return []
|
|
425
|
+
top = min(indents)
|
|
426
|
+
items: list[list[str]] = []
|
|
427
|
+
active: list[str] | None = None
|
|
428
|
+
for line, body in section:
|
|
429
|
+
match = re.match(r"^( {0,3})[-*+][ \t]+(.*)$", line) if body else None
|
|
430
|
+
if match and len(match.group(1)) == top:
|
|
431
|
+
if active is not None:
|
|
432
|
+
items.append(active)
|
|
433
|
+
active = [match.group(2)]
|
|
434
|
+
elif active is not None:
|
|
435
|
+
active.append(line)
|
|
436
|
+
if active is not None:
|
|
437
|
+
items.append(active)
|
|
438
|
+
return [_criterion("brief-example", source, "\n".join(item)) for item in items]
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _spec_acceptance(markdown: str, source: str) -> list[dict]:
|
|
442
|
+
criteria: list[dict] = []
|
|
443
|
+
ancestry: list[tuple[int, str]] = []
|
|
444
|
+
active: dict[str, Any] | None = None
|
|
445
|
+
|
|
446
|
+
def flush() -> None:
|
|
447
|
+
nonlocal active
|
|
448
|
+
if active is not None:
|
|
449
|
+
criteria.append(
|
|
450
|
+
_criterion(
|
|
451
|
+
"spec-scenario",
|
|
452
|
+
source,
|
|
453
|
+
"\n".join([active["title"], *active["body"]]),
|
|
454
|
+
active["context"],
|
|
455
|
+
)
|
|
456
|
+
)
|
|
457
|
+
active = None
|
|
458
|
+
|
|
459
|
+
for line, body in _scanned_markdown(markdown):
|
|
460
|
+
heading = _heading(line) if body else None
|
|
461
|
+
scenario = (
|
|
462
|
+
re.match(r"^Scenario\s*:\s*(.*)$", heading[1], re.IGNORECASE) if heading else None
|
|
463
|
+
)
|
|
464
|
+
if scenario:
|
|
465
|
+
flush()
|
|
466
|
+
while ancestry and ancestry[-1][0] >= heading[0]:
|
|
467
|
+
ancestry.pop()
|
|
468
|
+
title = _normalize_acceptance_text(scenario.group(1))
|
|
469
|
+
if not title:
|
|
470
|
+
raise NativeEvidenceError("Scenario title is empty")
|
|
471
|
+
active = {
|
|
472
|
+
"level": heading[0],
|
|
473
|
+
"title": title,
|
|
474
|
+
"body": [],
|
|
475
|
+
"context": [item[1] for item in ancestry],
|
|
476
|
+
}
|
|
477
|
+
continue
|
|
478
|
+
if heading:
|
|
479
|
+
if active is not None and heading[0] <= active["level"]:
|
|
480
|
+
flush()
|
|
481
|
+
while ancestry and ancestry[-1][0] >= heading[0]:
|
|
482
|
+
ancestry.pop()
|
|
483
|
+
ancestry.append((heading[0], _normalize_acceptance_text(heading[1])))
|
|
484
|
+
elif active is not None:
|
|
485
|
+
active["body"].append(line)
|
|
486
|
+
flush()
|
|
487
|
+
return criteria
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def build_contract_from_change(change_root: Path, state: dict[str, Any]) -> dict[str, Any]:
|
|
491
|
+
brief_ref = portable_ref(state.get("brief"), "Native brief ref")
|
|
492
|
+
brief_path = contained_file(change_root, brief_ref, "Native brief")
|
|
493
|
+
brief_markdown = brief_path.read_text(encoding="utf-8")
|
|
494
|
+
raw_specs = state.get("spec_changes")
|
|
495
|
+
if not isinstance(raw_specs, list) or not raw_specs or len(raw_specs) > 64:
|
|
496
|
+
raise NativeEvidenceError("Native spec_changes are missing or exceed their budget")
|
|
497
|
+
specs: list[dict[str, Any]] = []
|
|
498
|
+
acceptance = _brief_acceptance(brief_markdown, brief_ref)
|
|
499
|
+
seen_capabilities: set[str] = set()
|
|
500
|
+
seen_sources = {brief_ref}
|
|
501
|
+
for index, raw in enumerate(raw_specs):
|
|
502
|
+
spec = _record(raw, f"Native spec change {index}")
|
|
503
|
+
_exact_keys(
|
|
504
|
+
spec,
|
|
505
|
+
["capability", "operation", "source", "base_hash"],
|
|
506
|
+
label=f"Native spec change {index}",
|
|
507
|
+
)
|
|
508
|
+
capability = spec["capability"]
|
|
509
|
+
operation = spec["operation"]
|
|
510
|
+
if not isinstance(capability, str) or not CHANGE_NAME_PATTERN.fullmatch(capability):
|
|
511
|
+
raise NativeEvidenceError(f"Native spec change {index} capability is invalid")
|
|
512
|
+
if capability in seen_capabilities:
|
|
513
|
+
raise NativeEvidenceError("Native contract has duplicate capabilities")
|
|
514
|
+
seen_capabilities.add(capability)
|
|
515
|
+
if operation == "remove":
|
|
516
|
+
if spec["source"] is not None:
|
|
517
|
+
raise NativeEvidenceError("Remove spec cannot have a source")
|
|
518
|
+
base_hash = _hash(spec["base_hash"], "Remove spec base hash")
|
|
519
|
+
specs.append(
|
|
520
|
+
{
|
|
521
|
+
"capability": capability,
|
|
522
|
+
"operation": operation,
|
|
523
|
+
"source": None,
|
|
524
|
+
"baseHash": base_hash,
|
|
525
|
+
"contentHash": None,
|
|
526
|
+
}
|
|
527
|
+
)
|
|
528
|
+
continue
|
|
529
|
+
if operation not in {"create", "replace"}:
|
|
530
|
+
raise NativeEvidenceError(f"Native spec change {index} operation is invalid")
|
|
531
|
+
source = portable_ref(spec["source"], f"Native spec change {index} source")
|
|
532
|
+
if source in seen_sources:
|
|
533
|
+
raise NativeEvidenceError("Native contract has duplicate artifact sources")
|
|
534
|
+
seen_sources.add(source)
|
|
535
|
+
markdown = contained_file(change_root, source, "Native target spec").read_text(
|
|
536
|
+
encoding="utf-8"
|
|
537
|
+
)
|
|
538
|
+
base_hash = None if operation == "create" else _hash(spec["base_hash"], "Replace base hash")
|
|
539
|
+
if operation == "create" and spec["base_hash"] is not None:
|
|
540
|
+
raise NativeEvidenceError("Create spec must have a null base hash")
|
|
541
|
+
specs.append(
|
|
542
|
+
{
|
|
543
|
+
"capability": capability,
|
|
544
|
+
"operation": operation,
|
|
545
|
+
"source": source,
|
|
546
|
+
"baseHash": base_hash,
|
|
547
|
+
"contentHash": canonical_hash(
|
|
548
|
+
"comet.native.contract-content.v1",
|
|
549
|
+
markdown.replace("\r\n", "\n").replace("\r", "\n"),
|
|
550
|
+
),
|
|
551
|
+
}
|
|
552
|
+
)
|
|
553
|
+
acceptance.extend(_spec_acceptance(markdown, source))
|
|
554
|
+
acceptance.sort(key=lambda item: item["id"])
|
|
555
|
+
if not acceptance or len(acceptance) > 1024:
|
|
556
|
+
raise NativeEvidenceError("Native contract acceptance set is empty or over budget")
|
|
557
|
+
ids = [item["id"] for item in acceptance]
|
|
558
|
+
if len(ids) != len(set(ids)):
|
|
559
|
+
raise NativeEvidenceError("Native contract has duplicate acceptance IDs")
|
|
560
|
+
specs.sort(key=lambda item: item["capability"])
|
|
561
|
+
acceptance_hash = canonical_hash("comet.native.acceptance-set.v1", acceptance)
|
|
562
|
+
content = {
|
|
563
|
+
"schema": "comet.native.contract.v1",
|
|
564
|
+
"brief": {
|
|
565
|
+
"source": brief_ref,
|
|
566
|
+
"contentHash": canonical_hash(
|
|
567
|
+
"comet.native.contract-content.v1",
|
|
568
|
+
brief_markdown.replace("\r\n", "\n").replace("\r", "\n"),
|
|
569
|
+
),
|
|
570
|
+
},
|
|
571
|
+
"specs": specs,
|
|
572
|
+
"acceptance": acceptance,
|
|
573
|
+
"acceptanceHash": acceptance_hash,
|
|
574
|
+
}
|
|
575
|
+
return {**content, "contractHash": canonical_hash("comet.native.contract.v1", content)}
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _parse_snapshot_projection(value: Any, expected_hash: str) -> dict[str, Any]:
|
|
579
|
+
projection = _record(value, "Native snapshot projection")
|
|
580
|
+
_exact_keys(
|
|
581
|
+
projection,
|
|
582
|
+
["schema", "origin", "complete", "limits", "entries", "omitted", "omittedCount"],
|
|
583
|
+
["omissionOverflow"],
|
|
584
|
+
label="Native snapshot projection",
|
|
585
|
+
)
|
|
586
|
+
if projection["schema"] != "comet.native.content-snapshot-projection.v1":
|
|
587
|
+
raise NativeEvidenceError("Native snapshot projection schema is invalid")
|
|
588
|
+
if projection["origin"] not in {"change-created", "legacy-migration", "explicit"}:
|
|
589
|
+
raise NativeEvidenceError("Native snapshot projection origin is invalid")
|
|
590
|
+
if not isinstance(projection["complete"], bool):
|
|
591
|
+
raise NativeEvidenceError("Native snapshot projection completeness is invalid")
|
|
592
|
+
limits = _record(projection["limits"], "Native snapshot limits")
|
|
593
|
+
_exact_keys(
|
|
594
|
+
limits,
|
|
595
|
+
["maxFiles", "maxFileBytes", "maxTotalBytes", "maxManifestBytes"],
|
|
596
|
+
label="Native snapshot limits",
|
|
597
|
+
)
|
|
598
|
+
for key, value in limits.items():
|
|
599
|
+
_positive_int(value, f"Native snapshot {key}")
|
|
600
|
+
entries = projection["entries"]
|
|
601
|
+
omitted = projection["omitted"]
|
|
602
|
+
if not isinstance(entries, list) or not isinstance(omitted, list):
|
|
603
|
+
raise NativeEvidenceError("Native snapshot entries/omissions are invalid")
|
|
604
|
+
parsed_entries = []
|
|
605
|
+
for index, raw in enumerate(entries):
|
|
606
|
+
entry = _record(raw, f"Native snapshot entry {index}")
|
|
607
|
+
_exact_keys(entry, ["path", "hash", "size", "type"], label=f"Native snapshot entry {index}")
|
|
608
|
+
parsed_entries.append(
|
|
609
|
+
{
|
|
610
|
+
"path": portable_ref(entry["path"], f"Native snapshot entry {index} path"),
|
|
611
|
+
"hash": _hash(entry["hash"], f"Native snapshot entry {index} hash"),
|
|
612
|
+
"size": _nonnegative_int(entry["size"], f"Native snapshot entry {index} size"),
|
|
613
|
+
"type": entry["type"],
|
|
614
|
+
}
|
|
615
|
+
)
|
|
616
|
+
if entry["type"] != "file":
|
|
617
|
+
raise NativeEvidenceError(f"Native snapshot entry {index} type is invalid")
|
|
618
|
+
if len({entry["path"] for entry in parsed_entries}) != len(parsed_entries):
|
|
619
|
+
raise NativeEvidenceError("Native snapshot has duplicate paths")
|
|
620
|
+
if parsed_entries != sorted(
|
|
621
|
+
parsed_entries,
|
|
622
|
+
key=lambda item: (item["path"], item["hash"], item["size"]),
|
|
623
|
+
):
|
|
624
|
+
raise NativeEvidenceError("Native snapshot entries are not canonical")
|
|
625
|
+
if len(parsed_entries) > limits["maxFiles"] or any(
|
|
626
|
+
entry["size"] > limits["maxFileBytes"] for entry in parsed_entries
|
|
627
|
+
):
|
|
628
|
+
raise NativeEvidenceError("Native snapshot entries exceed their limits")
|
|
629
|
+
if sum(entry["size"] for entry in parsed_entries) > limits["maxTotalBytes"]:
|
|
630
|
+
raise NativeEvidenceError("Native snapshot entries exceed their total byte limit")
|
|
631
|
+
|
|
632
|
+
omission_types = {"file", "directory", "other"}
|
|
633
|
+
omission_reasons = {
|
|
634
|
+
"file-size",
|
|
635
|
+
"file-count",
|
|
636
|
+
"total-size",
|
|
637
|
+
"manifest-size",
|
|
638
|
+
"changed-during-read",
|
|
639
|
+
"unreadable",
|
|
640
|
+
}
|
|
641
|
+
parsed_omitted = []
|
|
642
|
+
for index, raw in enumerate(omitted):
|
|
643
|
+
omission = _record(raw, f"Native snapshot omission {index}")
|
|
644
|
+
_exact_keys(
|
|
645
|
+
omission,
|
|
646
|
+
["path", "size", "type", "reason"],
|
|
647
|
+
label=f"Native snapshot omission {index}",
|
|
648
|
+
)
|
|
649
|
+
if omission["type"] not in omission_types:
|
|
650
|
+
raise NativeEvidenceError(f"Native snapshot omission {index} type is invalid")
|
|
651
|
+
if omission["reason"] not in omission_reasons:
|
|
652
|
+
raise NativeEvidenceError(f"Native snapshot omission {index} reason is invalid")
|
|
653
|
+
parsed_omitted.append(
|
|
654
|
+
{
|
|
655
|
+
"path": portable_ref(omission["path"], f"Native snapshot omission {index} path"),
|
|
656
|
+
"size": (
|
|
657
|
+
None
|
|
658
|
+
if omission["size"] is None
|
|
659
|
+
else _nonnegative_int(
|
|
660
|
+
omission["size"], f"Native snapshot omission {index} size"
|
|
661
|
+
)
|
|
662
|
+
),
|
|
663
|
+
"type": omission["type"],
|
|
664
|
+
"reason": omission["reason"],
|
|
665
|
+
}
|
|
666
|
+
)
|
|
667
|
+
if parsed_omitted != sorted(
|
|
668
|
+
parsed_omitted,
|
|
669
|
+
key=lambda item: (
|
|
670
|
+
item["path"],
|
|
671
|
+
item["reason"],
|
|
672
|
+
item["type"],
|
|
673
|
+
-1 if item["size"] is None else item["size"],
|
|
674
|
+
),
|
|
675
|
+
):
|
|
676
|
+
raise NativeEvidenceError("Native snapshot omissions are not canonical")
|
|
677
|
+
omitted_count = _nonnegative_int(projection["omittedCount"], "Native omitted count")
|
|
678
|
+
if len(parsed_omitted) > 1000 or omitted_count < len(parsed_omitted):
|
|
679
|
+
raise NativeEvidenceError("Native snapshot omission count is invalid")
|
|
680
|
+
overflow_count = omitted_count - len(parsed_omitted)
|
|
681
|
+
overflow = projection.get("omissionOverflow")
|
|
682
|
+
if overflow is not None:
|
|
683
|
+
overflow = _record(overflow, "Native snapshot omission overflow")
|
|
684
|
+
_exact_keys(
|
|
685
|
+
overflow,
|
|
686
|
+
["ref", "hash", "count"],
|
|
687
|
+
label="Native snapshot omission overflow",
|
|
688
|
+
)
|
|
689
|
+
overflow_hash = _hash(overflow["hash"], "Native snapshot omission overflow hash")
|
|
690
|
+
if (
|
|
691
|
+
overflow["ref"] != f"native-snapshot://omitted-overflow/{overflow_hash}"
|
|
692
|
+
or _positive_int(overflow["count"], "Native snapshot omission overflow count")
|
|
693
|
+
!= overflow_count
|
|
694
|
+
):
|
|
695
|
+
raise NativeEvidenceError("Native snapshot omission overflow is inconsistent")
|
|
696
|
+
if (
|
|
697
|
+
(overflow_count == 0 and overflow is not None)
|
|
698
|
+
or (overflow_count > 0 and overflow is None)
|
|
699
|
+
or projection["complete"] != (omitted_count == 0)
|
|
700
|
+
):
|
|
701
|
+
raise NativeEvidenceError("Native snapshot omission state is inconsistent")
|
|
702
|
+
if canonical_hash("comet.native.content-snapshot-projection.v1", projection) != expected_hash:
|
|
703
|
+
raise NativeEvidenceError("Native snapshot projection content hash mismatch")
|
|
704
|
+
return projection
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _parse_declared_artifact(value: Any, label: str) -> dict[str, str]:
|
|
708
|
+
artifact = _record(value, label)
|
|
709
|
+
_exact_keys(artifact, ["path", "kind"], label=label)
|
|
710
|
+
if artifact["kind"] not in {"file", "directory"}:
|
|
711
|
+
raise NativeEvidenceError(f"{label} kind is invalid")
|
|
712
|
+
return {"path": portable_ref(artifact["path"], f"{label} path"), "kind": artifact["kind"]}
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _parse_identity(value: Any, label: str) -> dict[str, Any] | None:
|
|
716
|
+
if value is None:
|
|
717
|
+
return None
|
|
718
|
+
identity = _record(value, label)
|
|
719
|
+
_exact_keys(identity, ["hash", "size"], label=label)
|
|
720
|
+
return {
|
|
721
|
+
"hash": _hash(identity["hash"], f"{label} hash"),
|
|
722
|
+
"size": _nonnegative_int(identity["size"], f"{label} size"),
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _snapshot_changes(baseline: dict, current: dict) -> list[dict[str, Any]]:
|
|
727
|
+
before = {
|
|
728
|
+
entry["path"]: {"hash": entry["hash"], "size": entry["size"]}
|
|
729
|
+
for entry in baseline["entries"]
|
|
730
|
+
}
|
|
731
|
+
after = {
|
|
732
|
+
entry["path"]: {"hash": entry["hash"], "size": entry["size"]}
|
|
733
|
+
for entry in current["entries"]
|
|
734
|
+
}
|
|
735
|
+
result = []
|
|
736
|
+
for path in sorted(set(before) | set(after)):
|
|
737
|
+
if before.get(path) == after.get(path):
|
|
738
|
+
continue
|
|
739
|
+
result.append(
|
|
740
|
+
{
|
|
741
|
+
"path": path,
|
|
742
|
+
"kind": "added"
|
|
743
|
+
if path not in before
|
|
744
|
+
else "removed"
|
|
745
|
+
if path not in after
|
|
746
|
+
else "modified",
|
|
747
|
+
"before": before.get(path),
|
|
748
|
+
"after": after.get(path),
|
|
749
|
+
}
|
|
750
|
+
)
|
|
751
|
+
return result
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _artifact_covers(artifact: dict[str, str], path: str) -> bool:
|
|
755
|
+
return artifact["path"] == path or (
|
|
756
|
+
artifact["kind"] == "directory" and path.startswith(f"{artifact['path']}/")
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _derived_unresolved_scope(identity: dict[str, Any], reason: str) -> dict[str, Any]:
|
|
761
|
+
return {
|
|
762
|
+
"id": ("scope:" + canonical_hash("comet.native.unresolved-scope-id.v1", identity)),
|
|
763
|
+
"kind": identity["kind"],
|
|
764
|
+
"source": identity["source"],
|
|
765
|
+
"path": identity["path"],
|
|
766
|
+
"reason": reason,
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def _snapshot_omission_scopes(source: str, projection: dict[str, Any]) -> list[dict[str, Any]]:
|
|
771
|
+
scopes = [
|
|
772
|
+
_derived_unresolved_scope(
|
|
773
|
+
{
|
|
774
|
+
"kind": "snapshot-omission",
|
|
775
|
+
"source": source,
|
|
776
|
+
"path": omission["path"],
|
|
777
|
+
"evidence": {
|
|
778
|
+
"reason": omission["reason"],
|
|
779
|
+
"size": omission["size"],
|
|
780
|
+
"type": omission["type"],
|
|
781
|
+
},
|
|
782
|
+
},
|
|
783
|
+
f"{source} snapshot omitted {omission['path']}: {omission['reason']}",
|
|
784
|
+
)
|
|
785
|
+
for omission in projection["omitted"]
|
|
786
|
+
]
|
|
787
|
+
if not projection["complete"]:
|
|
788
|
+
scopes.append(
|
|
789
|
+
_derived_unresolved_scope(
|
|
790
|
+
{
|
|
791
|
+
"kind": "snapshot-incomplete",
|
|
792
|
+
"source": source,
|
|
793
|
+
"path": None,
|
|
794
|
+
"evidence": {"omittedCount": projection["omittedCount"]},
|
|
795
|
+
},
|
|
796
|
+
f"{source} snapshot is incomplete",
|
|
797
|
+
)
|
|
798
|
+
)
|
|
799
|
+
overflow = projection.get("omissionOverflow")
|
|
800
|
+
if overflow is not None:
|
|
801
|
+
scopes.append(
|
|
802
|
+
_derived_unresolved_scope(
|
|
803
|
+
{
|
|
804
|
+
"kind": "snapshot-omission-overflow",
|
|
805
|
+
"source": source,
|
|
806
|
+
"path": None,
|
|
807
|
+
"evidence": {
|
|
808
|
+
"count": overflow["count"],
|
|
809
|
+
"hash": overflow["hash"],
|
|
810
|
+
"ref": overflow["ref"],
|
|
811
|
+
},
|
|
812
|
+
},
|
|
813
|
+
f"{source} snapshot has {overflow['count']} unlisted omissions",
|
|
814
|
+
)
|
|
815
|
+
)
|
|
816
|
+
return scopes
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def _unresolved_sort_key(item: dict[str, Any]) -> tuple[str, str, tuple[int, str], str]:
|
|
820
|
+
path = item["path"]
|
|
821
|
+
return (
|
|
822
|
+
item["kind"],
|
|
823
|
+
item["source"],
|
|
824
|
+
(0, "") if path is None else (1, path),
|
|
825
|
+
item["id"],
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def parse_scope_bundle(
|
|
830
|
+
change_root: Path,
|
|
831
|
+
scope_ref: Any,
|
|
832
|
+
*,
|
|
833
|
+
project_root: Path | None = None,
|
|
834
|
+
) -> dict[str, Any]:
|
|
835
|
+
reference = portable_ref(scope_ref, "Implementation scope ref")
|
|
836
|
+
match = re.fullmatch(r"runtime/evidence/scopes/([a-f0-9]{64})\.json", reference)
|
|
837
|
+
if not match:
|
|
838
|
+
raise NativeEvidenceError("Implementation scope ref is not content addressed")
|
|
839
|
+
expected_hash = match.group(1)
|
|
840
|
+
scope = _record(
|
|
841
|
+
read_contained_json(change_root, reference, "Implementation scope"), "Implementation scope"
|
|
842
|
+
)
|
|
843
|
+
_exact_keys(
|
|
844
|
+
scope,
|
|
845
|
+
[
|
|
846
|
+
"schema",
|
|
847
|
+
"contractHash",
|
|
848
|
+
"baselineProjectionRef",
|
|
849
|
+
"baselineProjectionHash",
|
|
850
|
+
"currentProjectionRef",
|
|
851
|
+
"currentProjectionHash",
|
|
852
|
+
"complete",
|
|
853
|
+
"declaredArtifacts",
|
|
854
|
+
"changes",
|
|
855
|
+
"unattributed",
|
|
856
|
+
"unresolvedScopes",
|
|
857
|
+
"noCodeReason",
|
|
858
|
+
"scopeHash",
|
|
859
|
+
],
|
|
860
|
+
["gitAdvisory"],
|
|
861
|
+
label="Implementation scope",
|
|
862
|
+
)
|
|
863
|
+
if scope["schema"] != "comet.native.implementation-scope.v2":
|
|
864
|
+
raise NativeEvidenceError("Implementation scope schema is invalid")
|
|
865
|
+
if scope.get("gitAdvisory") is not None:
|
|
866
|
+
advisory = _record(scope["gitAdvisory"], "Implementation scope Git advisory")
|
|
867
|
+
_exact_keys(
|
|
868
|
+
advisory,
|
|
869
|
+
[
|
|
870
|
+
"advisoryOnly",
|
|
871
|
+
"changedPaths",
|
|
872
|
+
"pathsPresentInSnapshotChanges",
|
|
873
|
+
"pathsAbsentFromSnapshotChanges",
|
|
874
|
+
],
|
|
875
|
+
label="Implementation scope Git advisory",
|
|
876
|
+
)
|
|
877
|
+
if advisory["advisoryOnly"] is not True:
|
|
878
|
+
raise NativeEvidenceError("Implementation scope Git advisory is not advisory-only")
|
|
879
|
+
contract_hash = _hash(scope["contractHash"], "Implementation scope contract hash")
|
|
880
|
+
baseline_hash = _hash(scope["baselineProjectionHash"], "Baseline projection hash")
|
|
881
|
+
current_hash = _hash(scope["currentProjectionHash"], "Current projection hash")
|
|
882
|
+
expected_baseline_ref = f"runtime/evidence/snapshots/{baseline_hash}.json"
|
|
883
|
+
expected_current_ref = f"runtime/evidence/snapshots/{current_hash}.json"
|
|
884
|
+
if (
|
|
885
|
+
scope["baselineProjectionRef"] != expected_baseline_ref
|
|
886
|
+
or scope["currentProjectionRef"] != expected_current_ref
|
|
887
|
+
):
|
|
888
|
+
raise NativeEvidenceError("Implementation scope snapshot ref/hash binding is invalid")
|
|
889
|
+
baseline = _parse_snapshot_projection(
|
|
890
|
+
read_contained_json(change_root, expected_baseline_ref, "Baseline projection"),
|
|
891
|
+
baseline_hash,
|
|
892
|
+
)
|
|
893
|
+
current = _parse_snapshot_projection(
|
|
894
|
+
read_contained_json(change_root, expected_current_ref, "Current projection"), current_hash
|
|
895
|
+
)
|
|
896
|
+
if not isinstance(scope["complete"], bool):
|
|
897
|
+
raise NativeEvidenceError("Implementation scope completeness is invalid")
|
|
898
|
+
if not all(
|
|
899
|
+
isinstance(scope[key], list)
|
|
900
|
+
for key in ("declaredArtifacts", "changes", "unattributed", "unresolvedScopes")
|
|
901
|
+
):
|
|
902
|
+
raise NativeEvidenceError("Implementation scope collections are invalid")
|
|
903
|
+
declared = [
|
|
904
|
+
_parse_declared_artifact(item, f"Declared artifact {index}")
|
|
905
|
+
for index, item in enumerate(scope["declaredArtifacts"])
|
|
906
|
+
]
|
|
907
|
+
if declared != sorted(declared, key=lambda item: (item["path"], item["kind"])) or len(
|
|
908
|
+
{item["path"] for item in declared}
|
|
909
|
+
) != len(declared):
|
|
910
|
+
raise NativeEvidenceError("Declared artifacts are not sorted and unique")
|
|
911
|
+
expected_changes = _snapshot_changes(baseline, current)
|
|
912
|
+
parsed_changes = []
|
|
913
|
+
for index, raw in enumerate(scope["changes"]):
|
|
914
|
+
change = _record(raw, f"Implementation change {index}")
|
|
915
|
+
_exact_keys(
|
|
916
|
+
change,
|
|
917
|
+
["path", "kind", "before", "after", "attributedTo"],
|
|
918
|
+
label=f"Implementation change {index}",
|
|
919
|
+
)
|
|
920
|
+
path = portable_ref(change["path"], f"Implementation change {index} path")
|
|
921
|
+
if not isinstance(change["attributedTo"], list):
|
|
922
|
+
raise NativeEvidenceError(f"Implementation change {index} attribution must be an array")
|
|
923
|
+
attributed = [
|
|
924
|
+
_parse_declared_artifact(
|
|
925
|
+
item, f"Implementation change {index} attribution {item_index}"
|
|
926
|
+
)
|
|
927
|
+
for item_index, item in enumerate(change["attributedTo"])
|
|
928
|
+
]
|
|
929
|
+
expected_attribution = [item for item in declared if _artifact_covers(item, path)]
|
|
930
|
+
parsed_changes.append(
|
|
931
|
+
{
|
|
932
|
+
"path": path,
|
|
933
|
+
"kind": change["kind"],
|
|
934
|
+
"before": _parse_identity(
|
|
935
|
+
change["before"], f"Implementation change {index} before"
|
|
936
|
+
),
|
|
937
|
+
"after": _parse_identity(change["after"], f"Implementation change {index} after"),
|
|
938
|
+
"attributedTo": attributed,
|
|
939
|
+
}
|
|
940
|
+
)
|
|
941
|
+
if attributed != expected_attribution:
|
|
942
|
+
raise NativeEvidenceError(f"Implementation change {path} attribution is forged")
|
|
943
|
+
expected_with_attribution = [
|
|
944
|
+
{
|
|
945
|
+
**item,
|
|
946
|
+
"attributedTo": [
|
|
947
|
+
artifact for artifact in declared if _artifact_covers(artifact, item["path"])
|
|
948
|
+
],
|
|
949
|
+
}
|
|
950
|
+
for item in expected_changes
|
|
951
|
+
]
|
|
952
|
+
if parsed_changes != expected_with_attribution:
|
|
953
|
+
raise NativeEvidenceError("Implementation scope changes do not match its snapshots")
|
|
954
|
+
expected_unattributed = [item for item in parsed_changes if not item["attributedTo"]]
|
|
955
|
+
if scope["unattributed"] != expected_unattributed:
|
|
956
|
+
raise NativeEvidenceError("Implementation scope unattributed changes are inconsistent")
|
|
957
|
+
no_code_reason = scope["noCodeReason"]
|
|
958
|
+
if no_code_reason is not None and (
|
|
959
|
+
not isinstance(no_code_reason, str)
|
|
960
|
+
or not no_code_reason
|
|
961
|
+
or no_code_reason != no_code_reason.strip()
|
|
962
|
+
):
|
|
963
|
+
raise NativeEvidenceError("Implementation scope no-code reason is invalid")
|
|
964
|
+
|
|
965
|
+
unresolved_kinds = {
|
|
966
|
+
"unattributed-change",
|
|
967
|
+
"snapshot-omission",
|
|
968
|
+
"snapshot-incomplete",
|
|
969
|
+
"snapshot-omission-overflow",
|
|
970
|
+
"missing-no-code-reason",
|
|
971
|
+
}
|
|
972
|
+
unresolved_sources = {"baseline", "current", "implementation-scope"}
|
|
973
|
+
parsed_unresolved = []
|
|
974
|
+
for index, raw in enumerate(scope["unresolvedScopes"]):
|
|
975
|
+
unresolved = _record(raw, f"Unresolved scope {index}")
|
|
976
|
+
_exact_keys(
|
|
977
|
+
unresolved,
|
|
978
|
+
["id", "kind", "source", "path", "reason"],
|
|
979
|
+
label=f"Unresolved scope {index}",
|
|
980
|
+
)
|
|
981
|
+
if not isinstance(unresolved["id"], str) or not SCOPE_ID_PATTERN.fullmatch(
|
|
982
|
+
unresolved["id"]
|
|
983
|
+
):
|
|
984
|
+
raise NativeEvidenceError(f"Unresolved scope {index} id is invalid")
|
|
985
|
+
if unresolved["kind"] not in unresolved_kinds:
|
|
986
|
+
raise NativeEvidenceError(f"Unresolved scope {index} kind is invalid")
|
|
987
|
+
if unresolved["source"] not in unresolved_sources:
|
|
988
|
+
raise NativeEvidenceError(f"Unresolved scope {index} source is invalid")
|
|
989
|
+
path = unresolved["path"]
|
|
990
|
+
if path is not None:
|
|
991
|
+
path = portable_ref(path, f"Unresolved scope {index} path")
|
|
992
|
+
reason = unresolved["reason"]
|
|
993
|
+
if not isinstance(reason, str) or not reason or reason != reason.strip():
|
|
994
|
+
raise NativeEvidenceError(f"Unresolved scope {index} reason is invalid")
|
|
995
|
+
parsed_unresolved.append(
|
|
996
|
+
{
|
|
997
|
+
"id": unresolved["id"],
|
|
998
|
+
"kind": unresolved["kind"],
|
|
999
|
+
"source": unresolved["source"],
|
|
1000
|
+
"path": path,
|
|
1001
|
+
"reason": reason,
|
|
1002
|
+
}
|
|
1003
|
+
)
|
|
1004
|
+
|
|
1005
|
+
expected_unresolved = [
|
|
1006
|
+
*(
|
|
1007
|
+
_derived_unresolved_scope(
|
|
1008
|
+
{
|
|
1009
|
+
"kind": "unattributed-change",
|
|
1010
|
+
"source": "implementation-scope",
|
|
1011
|
+
"path": change["path"],
|
|
1012
|
+
"evidence": {
|
|
1013
|
+
"after": change["after"],
|
|
1014
|
+
"before": change["before"],
|
|
1015
|
+
"changeKind": change["kind"],
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
f"Changed path is not covered by a declared artifact: {change['path']}",
|
|
1019
|
+
)
|
|
1020
|
+
for change in expected_unattributed
|
|
1021
|
+
),
|
|
1022
|
+
*_snapshot_omission_scopes("baseline", baseline),
|
|
1023
|
+
*_snapshot_omission_scopes("current", current),
|
|
1024
|
+
]
|
|
1025
|
+
if not parsed_changes and no_code_reason is None:
|
|
1026
|
+
expected_unresolved.append(
|
|
1027
|
+
_derived_unresolved_scope(
|
|
1028
|
+
{
|
|
1029
|
+
"kind": "missing-no-code-reason",
|
|
1030
|
+
"source": "implementation-scope",
|
|
1031
|
+
"path": None,
|
|
1032
|
+
"evidence": {
|
|
1033
|
+
"baselineProjectionHash": baseline_hash,
|
|
1034
|
+
"currentProjectionHash": current_hash,
|
|
1035
|
+
},
|
|
1036
|
+
},
|
|
1037
|
+
"A non-empty no-code reason is required when the snapshots contain no changes",
|
|
1038
|
+
)
|
|
1039
|
+
)
|
|
1040
|
+
expected_unresolved = sorted(
|
|
1041
|
+
{item["id"]: item for item in expected_unresolved}.values(),
|
|
1042
|
+
key=_unresolved_sort_key,
|
|
1043
|
+
)
|
|
1044
|
+
unresolved_ids = [item["id"] for item in parsed_unresolved]
|
|
1045
|
+
if (
|
|
1046
|
+
len(unresolved_ids) != len(set(unresolved_ids))
|
|
1047
|
+
or parsed_unresolved != sorted(parsed_unresolved, key=_unresolved_sort_key)
|
|
1048
|
+
or parsed_unresolved != expected_unresolved
|
|
1049
|
+
):
|
|
1050
|
+
raise NativeEvidenceError(
|
|
1051
|
+
"Implementation scope unresolved entries do not match the Runtime-derived scope"
|
|
1052
|
+
)
|
|
1053
|
+
if scope["complete"] != (len(parsed_unresolved) == 0):
|
|
1054
|
+
raise NativeEvidenceError("Implementation scope completeness is inconsistent")
|
|
1055
|
+
content = {key: value for key, value in scope.items() if key != "scopeHash"}
|
|
1056
|
+
scope_hash = _hash(scope["scopeHash"], "Implementation scope hash")
|
|
1057
|
+
if (
|
|
1058
|
+
scope_hash != expected_hash
|
|
1059
|
+
or canonical_hash("comet.native.implementation-scope.v2", content) != scope_hash
|
|
1060
|
+
):
|
|
1061
|
+
raise NativeEvidenceError("Implementation scope filename/content hash mismatch")
|
|
1062
|
+
if project_root is not None:
|
|
1063
|
+
for entry in current["entries"]:
|
|
1064
|
+
target = contained_file(project_root, entry["path"], "Current snapshot project entry")
|
|
1065
|
+
if target.stat().st_size != entry["size"] or sha256_file(target) != entry["hash"]:
|
|
1066
|
+
raise NativeEvidenceError(
|
|
1067
|
+
f"Current snapshot project entry drifted: {entry['path']}"
|
|
1068
|
+
)
|
|
1069
|
+
return {"scope": scope, "baseline": baseline, "current": current, "contractHash": contract_hash}
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
def parse_partial_allowance(
|
|
1073
|
+
change_root: Path,
|
|
1074
|
+
allowance_ref: Any,
|
|
1075
|
+
*,
|
|
1076
|
+
expected_change: str,
|
|
1077
|
+
) -> dict[str, Any]:
|
|
1078
|
+
reference = portable_ref(allowance_ref, "Partial allowance ref")
|
|
1079
|
+
match = re.fullmatch(r"runtime/evidence/allowances/([a-f0-9]{64})\.json", reference)
|
|
1080
|
+
if not match:
|
|
1081
|
+
raise NativeEvidenceError("Partial allowance ref is not content addressed")
|
|
1082
|
+
allowance = _record(
|
|
1083
|
+
read_contained_json(change_root, reference, "Partial allowance"), "Partial allowance"
|
|
1084
|
+
)
|
|
1085
|
+
_exact_keys(
|
|
1086
|
+
allowance,
|
|
1087
|
+
[
|
|
1088
|
+
"schema",
|
|
1089
|
+
"change",
|
|
1090
|
+
"scopeHash",
|
|
1091
|
+
"scopeIds",
|
|
1092
|
+
"reason",
|
|
1093
|
+
"confirmedSummary",
|
|
1094
|
+
"sourceRevision",
|
|
1095
|
+
"confirmedAt",
|
|
1096
|
+
"allowanceHash",
|
|
1097
|
+
],
|
|
1098
|
+
label="Partial allowance",
|
|
1099
|
+
)
|
|
1100
|
+
if (
|
|
1101
|
+
allowance["schema"] != "comet.native.partial-allowance.v1"
|
|
1102
|
+
or allowance["change"] != expected_change
|
|
1103
|
+
):
|
|
1104
|
+
raise NativeEvidenceError("Partial allowance schema/change binding is invalid")
|
|
1105
|
+
scope_hash = _hash(allowance["scopeHash"], "Partial allowance scope hash")
|
|
1106
|
+
scope_ids = allowance["scopeIds"]
|
|
1107
|
+
if (
|
|
1108
|
+
not isinstance(scope_ids, list)
|
|
1109
|
+
or not scope_ids
|
|
1110
|
+
or any(
|
|
1111
|
+
not isinstance(item, str) or not SCOPE_ID_PATTERN.fullmatch(item) for item in scope_ids
|
|
1112
|
+
)
|
|
1113
|
+
or scope_ids != sorted(set(scope_ids))
|
|
1114
|
+
):
|
|
1115
|
+
raise NativeEvidenceError("Partial allowance scope IDs are invalid")
|
|
1116
|
+
for key in ("reason", "confirmedSummary"):
|
|
1117
|
+
if (
|
|
1118
|
+
not isinstance(allowance[key], str)
|
|
1119
|
+
or not allowance[key]
|
|
1120
|
+
or allowance[key] != allowance[key].strip()
|
|
1121
|
+
):
|
|
1122
|
+
raise NativeEvidenceError(f"Partial allowance {key} is invalid")
|
|
1123
|
+
_positive_int(allowance["sourceRevision"], "Partial allowance source revision")
|
|
1124
|
+
_timestamp(allowance["confirmedAt"], "Partial allowance timestamp")
|
|
1125
|
+
content = {key: value for key, value in allowance.items() if key != "allowanceHash"}
|
|
1126
|
+
allowance_hash = _hash(allowance["allowanceHash"], "Partial allowance hash")
|
|
1127
|
+
if (
|
|
1128
|
+
allowance_hash != match.group(1)
|
|
1129
|
+
or canonical_hash("comet.native.partial-allowance.v1", content) != allowance_hash
|
|
1130
|
+
):
|
|
1131
|
+
raise NativeEvidenceError("Partial allowance filename/content hash mismatch")
|
|
1132
|
+
scope_bundle = parse_scope_bundle(change_root, f"runtime/evidence/scopes/{scope_hash}.json")
|
|
1133
|
+
scope = scope_bundle["scope"]
|
|
1134
|
+
expected_ids = sorted(item["id"] for item in scope["unresolvedScopes"])
|
|
1135
|
+
if scope["complete"] or scope_ids != expected_ids:
|
|
1136
|
+
raise NativeEvidenceError("Partial allowance does not bind the unresolved scope")
|
|
1137
|
+
return allowance
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
def _parse_acceptance_trace(
|
|
1141
|
+
value: Any,
|
|
1142
|
+
*,
|
|
1143
|
+
contract: dict[str, Any],
|
|
1144
|
+
project_root: Path,
|
|
1145
|
+
) -> dict[str, Any]:
|
|
1146
|
+
trace = _record(value, "Native acceptance trace")
|
|
1147
|
+
_exact_keys(
|
|
1148
|
+
trace,
|
|
1149
|
+
[
|
|
1150
|
+
"schema",
|
|
1151
|
+
"nativeRootRef",
|
|
1152
|
+
"criteriaHash",
|
|
1153
|
+
"total",
|
|
1154
|
+
"evidenced",
|
|
1155
|
+
"skipped",
|
|
1156
|
+
"entries",
|
|
1157
|
+
"traceHash",
|
|
1158
|
+
],
|
|
1159
|
+
label="Native acceptance trace",
|
|
1160
|
+
)
|
|
1161
|
+
if trace["schema"] != "comet.native.acceptance-trace.v1":
|
|
1162
|
+
raise NativeEvidenceError("Native acceptance trace schema is invalid")
|
|
1163
|
+
native_root_ref = portable_ref(trace["nativeRootRef"], "Native root ref")
|
|
1164
|
+
criteria_hash = _hash(trace["criteriaHash"], "Acceptance criteria hash")
|
|
1165
|
+
if criteria_hash != contract["acceptanceHash"]:
|
|
1166
|
+
raise NativeEvidenceError("Acceptance trace is not bound to the contract")
|
|
1167
|
+
entries = trace["entries"]
|
|
1168
|
+
if not isinstance(entries, list):
|
|
1169
|
+
raise NativeEvidenceError("Acceptance trace entries are invalid")
|
|
1170
|
+
expected_by_id = {item["id"]: item for item in contract["acceptance"]}
|
|
1171
|
+
parsed = []
|
|
1172
|
+
for index, raw in enumerate(entries):
|
|
1173
|
+
entry = _record(raw, f"Acceptance trace entry {index}")
|
|
1174
|
+
_exact_keys(
|
|
1175
|
+
entry,
|
|
1176
|
+
["acceptanceId", "kind", "source", "evidenceRefs", "skippedReason"],
|
|
1177
|
+
label=f"Acceptance trace entry {index}",
|
|
1178
|
+
)
|
|
1179
|
+
acceptance_id = entry["acceptanceId"]
|
|
1180
|
+
if not isinstance(acceptance_id, str) or not ACCEPTANCE_ID_PATTERN.fullmatch(acceptance_id):
|
|
1181
|
+
raise NativeEvidenceError(f"Acceptance trace entry {index} has a forged ID")
|
|
1182
|
+
criterion = expected_by_id.get(acceptance_id)
|
|
1183
|
+
if (
|
|
1184
|
+
criterion is None
|
|
1185
|
+
or entry["kind"] != criterion["kind"]
|
|
1186
|
+
or entry["source"] != criterion["source"]
|
|
1187
|
+
):
|
|
1188
|
+
raise NativeEvidenceError(f"Acceptance trace entry {index} is not in the contract")
|
|
1189
|
+
refs = entry["evidenceRefs"]
|
|
1190
|
+
if (
|
|
1191
|
+
not isinstance(refs, list)
|
|
1192
|
+
or any(not isinstance(ref, str) for ref in refs)
|
|
1193
|
+
or refs != sorted(set(refs))
|
|
1194
|
+
):
|
|
1195
|
+
raise NativeEvidenceError(f"Acceptance trace entry {index} refs are not canonical")
|
|
1196
|
+
skipped = entry["skippedReason"]
|
|
1197
|
+
if (not refs) == (skipped is None):
|
|
1198
|
+
raise NativeEvidenceError(f"Acceptance trace entry {index} evidence state is invalid")
|
|
1199
|
+
if skipped is not None and (
|
|
1200
|
+
not isinstance(skipped, str) or not skipped or skipped != skipped.strip()
|
|
1201
|
+
):
|
|
1202
|
+
raise NativeEvidenceError(f"Acceptance trace entry {index} skipped reason is invalid")
|
|
1203
|
+
for evidence_ref in refs:
|
|
1204
|
+
reference = portable_ref(evidence_ref, f"Acceptance evidence ref {acceptance_id}")
|
|
1205
|
+
lower = reference.lower()
|
|
1206
|
+
if (
|
|
1207
|
+
lower.startswith((".cache/", ".git/", ".env"))
|
|
1208
|
+
or lower == native_root_ref.lower()
|
|
1209
|
+
or lower.startswith(f"{native_root_ref.lower()}/")
|
|
1210
|
+
):
|
|
1211
|
+
raise NativeEvidenceError(
|
|
1212
|
+
f"Acceptance evidence ref is workflow/sensitive metadata: {reference}"
|
|
1213
|
+
)
|
|
1214
|
+
contained_file(project_root, reference, f"Acceptance evidence ref {acceptance_id}")
|
|
1215
|
+
parsed.append(entry)
|
|
1216
|
+
if parsed != sorted(parsed, key=lambda item: item["acceptanceId"]) or len(
|
|
1217
|
+
{item["acceptanceId"] for item in parsed}
|
|
1218
|
+
) != len(parsed):
|
|
1219
|
+
raise NativeEvidenceError("Acceptance trace entries are not sorted and unique")
|
|
1220
|
+
if set(item["acceptanceId"] for item in parsed) != set(expected_by_id):
|
|
1221
|
+
raise NativeEvidenceError("Acceptance trace does not cover the exact contract")
|
|
1222
|
+
total = _nonnegative_int(trace["total"], "Acceptance trace total")
|
|
1223
|
+
evidenced = _nonnegative_int(trace["evidenced"], "Acceptance trace evidenced")
|
|
1224
|
+
skipped_count = _nonnegative_int(trace["skipped"], "Acceptance trace skipped")
|
|
1225
|
+
if (
|
|
1226
|
+
total != len(parsed)
|
|
1227
|
+
or evidenced != sum(bool(item["evidenceRefs"]) for item in parsed)
|
|
1228
|
+
or skipped_count != sum(item["skippedReason"] is not None for item in parsed)
|
|
1229
|
+
):
|
|
1230
|
+
raise NativeEvidenceError("Acceptance trace counts are inconsistent")
|
|
1231
|
+
content = {key: value for key, value in trace.items() if key != "traceHash"}
|
|
1232
|
+
trace_hash = _hash(trace["traceHash"], "Acceptance trace hash")
|
|
1233
|
+
if canonical_hash("comet.native.acceptance-trace.v1", content) != trace_hash:
|
|
1234
|
+
raise NativeEvidenceError("Acceptance trace content hash mismatch")
|
|
1235
|
+
return trace
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
def _parse_check_receipt(
|
|
1239
|
+
*,
|
|
1240
|
+
change_root: Path,
|
|
1241
|
+
receipt_ref: Any,
|
|
1242
|
+
expected_change: str,
|
|
1243
|
+
expected_source_revision: int,
|
|
1244
|
+
expected_result: str,
|
|
1245
|
+
expected_contract_hash: str,
|
|
1246
|
+
scope: dict[str, Any],
|
|
1247
|
+
) -> dict[str, Any]:
|
|
1248
|
+
reference = portable_ref(receipt_ref, "Verification receipt ref")
|
|
1249
|
+
match = re.fullmatch(r"runtime/evidence/check-receipts/([a-f0-9]{64})\.json", reference)
|
|
1250
|
+
if not match:
|
|
1251
|
+
raise NativeEvidenceError("Verification receipt ref is invalid")
|
|
1252
|
+
receipt_file = contained_file(change_root, reference, "Verification receipt")
|
|
1253
|
+
if receipt_file.stat().st_size > 512 * 1024:
|
|
1254
|
+
raise NativeEvidenceError("Native check receipt exceeds its byte budget")
|
|
1255
|
+
try:
|
|
1256
|
+
receipt = _record(
|
|
1257
|
+
json.loads(receipt_file.read_text(encoding="utf-8")),
|
|
1258
|
+
"Native check receipt",
|
|
1259
|
+
)
|
|
1260
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
1261
|
+
raise NativeEvidenceError(f"Native check receipt is invalid JSON: {error}") from error
|
|
1262
|
+
_exact_keys(
|
|
1263
|
+
receipt,
|
|
1264
|
+
[
|
|
1265
|
+
"schema",
|
|
1266
|
+
"change",
|
|
1267
|
+
"sourceRevision",
|
|
1268
|
+
"checker",
|
|
1269
|
+
"inputHash",
|
|
1270
|
+
"status",
|
|
1271
|
+
"startedAt",
|
|
1272
|
+
"endedAt",
|
|
1273
|
+
"contract",
|
|
1274
|
+
"implementation",
|
|
1275
|
+
"counts",
|
|
1276
|
+
"issues",
|
|
1277
|
+
"issuesTruncated",
|
|
1278
|
+
"stale",
|
|
1279
|
+
"staleReasons",
|
|
1280
|
+
"receiptHash",
|
|
1281
|
+
],
|
|
1282
|
+
label="Native check receipt",
|
|
1283
|
+
)
|
|
1284
|
+
change = receipt["change"]
|
|
1285
|
+
if (
|
|
1286
|
+
receipt["schema"] != "comet.native.check-receipt.v1"
|
|
1287
|
+
or not isinstance(change, str)
|
|
1288
|
+
or len(change.encode("utf-8")) > 128
|
|
1289
|
+
or not CHANGE_NAME_PATTERN.fullmatch(change)
|
|
1290
|
+
):
|
|
1291
|
+
raise NativeEvidenceError("Native check receipt schema/change is invalid")
|
|
1292
|
+
source_revision = _positive_int(
|
|
1293
|
+
receipt["sourceRevision"], "Native check receipt source revision"
|
|
1294
|
+
)
|
|
1295
|
+
|
|
1296
|
+
checker = _record(receipt["checker"], "Native check receipt checker")
|
|
1297
|
+
_exact_keys(
|
|
1298
|
+
checker,
|
|
1299
|
+
["policy", "version", "hash", "limits"],
|
|
1300
|
+
label="Native check receipt checker",
|
|
1301
|
+
)
|
|
1302
|
+
limits = _record(checker["limits"], "Native check receipt limits")
|
|
1303
|
+
_exact_keys(
|
|
1304
|
+
limits,
|
|
1305
|
+
["maxFiles", "maxFileBytes", "maxTotalBytes", "maxIssues"],
|
|
1306
|
+
label="Native check receipt limits",
|
|
1307
|
+
)
|
|
1308
|
+
if (
|
|
1309
|
+
checker["policy"] != "scoped-text-safety"
|
|
1310
|
+
or isinstance(checker["version"], bool)
|
|
1311
|
+
or not isinstance(checker["version"], int)
|
|
1312
|
+
or checker["version"] != 1
|
|
1313
|
+
or checker["hash"] != NATIVE_CHECKER_HASH
|
|
1314
|
+
or any(
|
|
1315
|
+
isinstance(limits[key], bool) or not isinstance(limits[key], int)
|
|
1316
|
+
for key in NATIVE_CHECK_LIMITS
|
|
1317
|
+
)
|
|
1318
|
+
or limits != NATIVE_CHECK_LIMITS
|
|
1319
|
+
):
|
|
1320
|
+
raise NativeEvidenceError("Native check receipt checker policy is unsupported")
|
|
1321
|
+
|
|
1322
|
+
contract = _record(receipt["contract"], "Native check receipt contract")
|
|
1323
|
+
_exact_keys(
|
|
1324
|
+
contract,
|
|
1325
|
+
["expectedHash", "beforeHash", "afterHash"],
|
|
1326
|
+
label="Native check receipt contract",
|
|
1327
|
+
)
|
|
1328
|
+
parsed_contract = {
|
|
1329
|
+
"expectedHash": _hash(
|
|
1330
|
+
contract["expectedHash"], "Native check receipt expected contract hash"
|
|
1331
|
+
),
|
|
1332
|
+
"beforeHash": _hash(contract["beforeHash"], "Native check receipt before contract hash"),
|
|
1333
|
+
"afterHash": _hash(contract["afterHash"], "Native check receipt after contract hash"),
|
|
1334
|
+
}
|
|
1335
|
+
implementation = _record(receipt["implementation"], "Native check receipt implementation")
|
|
1336
|
+
_exact_keys(
|
|
1337
|
+
implementation,
|
|
1338
|
+
[
|
|
1339
|
+
"scopeHash",
|
|
1340
|
+
"expectedSnapshotHash",
|
|
1341
|
+
"beforeSnapshotHash",
|
|
1342
|
+
"afterSnapshotHash",
|
|
1343
|
+
],
|
|
1344
|
+
label="Native check receipt implementation",
|
|
1345
|
+
)
|
|
1346
|
+
parsed_implementation = {
|
|
1347
|
+
"scopeHash": _hash(implementation["scopeHash"], "Native check receipt scope hash"),
|
|
1348
|
+
"expectedSnapshotHash": _hash(
|
|
1349
|
+
implementation["expectedSnapshotHash"],
|
|
1350
|
+
"Native check receipt expected snapshot hash",
|
|
1351
|
+
),
|
|
1352
|
+
"beforeSnapshotHash": _hash(
|
|
1353
|
+
implementation["beforeSnapshotHash"],
|
|
1354
|
+
"Native check receipt before snapshot hash",
|
|
1355
|
+
),
|
|
1356
|
+
"afterSnapshotHash": _hash(
|
|
1357
|
+
implementation["afterSnapshotHash"],
|
|
1358
|
+
"Native check receipt after snapshot hash",
|
|
1359
|
+
),
|
|
1360
|
+
}
|
|
1361
|
+
expected_input_hash = canonical_hash(
|
|
1362
|
+
"comet.native.check-input.v1",
|
|
1363
|
+
{
|
|
1364
|
+
"change": change,
|
|
1365
|
+
"sourceRevision": source_revision,
|
|
1366
|
+
"checkerHash": checker["hash"],
|
|
1367
|
+
"contractHash": parsed_contract["expectedHash"],
|
|
1368
|
+
"scopeHash": parsed_implementation["scopeHash"],
|
|
1369
|
+
"snapshotHash": parsed_implementation["expectedSnapshotHash"],
|
|
1370
|
+
},
|
|
1371
|
+
)
|
|
1372
|
+
if _hash(receipt["inputHash"], "Native check receipt input hash") != expected_input_hash:
|
|
1373
|
+
raise NativeEvidenceError("Native check receipt input hash mismatch")
|
|
1374
|
+
if receipt["status"] not in {"passed", "failed"}:
|
|
1375
|
+
raise NativeEvidenceError("Native check receipt status is invalid")
|
|
1376
|
+
started_at = _timestamp(receipt["startedAt"], "Native check receipt startedAt")
|
|
1377
|
+
ended_at = _timestamp(receipt["endedAt"], "Native check receipt endedAt")
|
|
1378
|
+
if ended_at < started_at:
|
|
1379
|
+
raise NativeEvidenceError("Native check receipt endedAt precedes startedAt")
|
|
1380
|
+
|
|
1381
|
+
counts = _record(receipt["counts"], "Native check receipt counts")
|
|
1382
|
+
_exact_keys(
|
|
1383
|
+
counts,
|
|
1384
|
+
[
|
|
1385
|
+
"filesSelected",
|
|
1386
|
+
"filesScanned",
|
|
1387
|
+
"binaryFilesSkipped",
|
|
1388
|
+
"bytesScanned",
|
|
1389
|
+
"issueCount",
|
|
1390
|
+
"recordedIssueCount",
|
|
1391
|
+
],
|
|
1392
|
+
label="Native check receipt counts",
|
|
1393
|
+
)
|
|
1394
|
+
parsed_counts = {
|
|
1395
|
+
key: _nonnegative_int(value, f"Native check receipt {key}") for key, value in counts.items()
|
|
1396
|
+
}
|
|
1397
|
+
if (
|
|
1398
|
+
parsed_counts["filesScanned"] + parsed_counts["binaryFilesSkipped"]
|
|
1399
|
+
> parsed_counts["filesSelected"]
|
|
1400
|
+
or parsed_counts["filesScanned"] + parsed_counts["binaryFilesSkipped"]
|
|
1401
|
+
> NATIVE_CHECK_LIMITS["maxFiles"]
|
|
1402
|
+
or parsed_counts["bytesScanned"] > NATIVE_CHECK_LIMITS["maxTotalBytes"]
|
|
1403
|
+
or parsed_counts["recordedIssueCount"] > parsed_counts["issueCount"]
|
|
1404
|
+
or parsed_counts["recordedIssueCount"] > NATIVE_CHECK_LIMITS["maxIssues"]
|
|
1405
|
+
):
|
|
1406
|
+
raise NativeEvidenceError("Native check receipt count accounting is invalid")
|
|
1407
|
+
|
|
1408
|
+
raw_issues = receipt["issues"]
|
|
1409
|
+
if not isinstance(raw_issues, list) or len(raw_issues) > NATIVE_CHECK_LIMITS["maxIssues"]:
|
|
1410
|
+
raise NativeEvidenceError("Native check receipt issues are not a bounded array")
|
|
1411
|
+
issue_rank = {kind: index for index, kind in enumerate(NATIVE_CHECK_ISSUE_KINDS)}
|
|
1412
|
+
issues = []
|
|
1413
|
+
for index, raw in enumerate(raw_issues):
|
|
1414
|
+
issue = _record(raw, f"Native check receipt issue {index}")
|
|
1415
|
+
_exact_keys(
|
|
1416
|
+
issue,
|
|
1417
|
+
["path", "line", "kind"],
|
|
1418
|
+
label=f"Native check receipt issue {index}",
|
|
1419
|
+
)
|
|
1420
|
+
path = portable_ref(issue["path"], f"Native check receipt issue {index} path")
|
|
1421
|
+
if len(path.encode("utf-8")) > 2048:
|
|
1422
|
+
raise NativeEvidenceError(f"Native check receipt issue {index} path is too long")
|
|
1423
|
+
if issue["kind"] not in issue_rank:
|
|
1424
|
+
raise NativeEvidenceError(f"Native check receipt issue {index} kind is invalid")
|
|
1425
|
+
issues.append(
|
|
1426
|
+
{
|
|
1427
|
+
"path": path,
|
|
1428
|
+
"line": _positive_int(issue["line"], f"Native check receipt issue {index} line"),
|
|
1429
|
+
"kind": issue["kind"],
|
|
1430
|
+
}
|
|
1431
|
+
)
|
|
1432
|
+
if issues != sorted(
|
|
1433
|
+
issues,
|
|
1434
|
+
key=lambda issue: (issue["path"], issue["line"], issue_rank[issue["kind"]]),
|
|
1435
|
+
):
|
|
1436
|
+
raise NativeEvidenceError("Native check receipt issues are not canonical")
|
|
1437
|
+
if parsed_counts["recordedIssueCount"] != len(issues):
|
|
1438
|
+
raise NativeEvidenceError("Native check receipt recorded issue count is inconsistent")
|
|
1439
|
+
if not isinstance(receipt["issuesTruncated"], bool) or receipt["issuesTruncated"] != (
|
|
1440
|
+
parsed_counts["issueCount"] > len(issues)
|
|
1441
|
+
):
|
|
1442
|
+
raise NativeEvidenceError("Native check receipt issue truncation is inconsistent")
|
|
1443
|
+
|
|
1444
|
+
stale_reasons = receipt["staleReasons"]
|
|
1445
|
+
if (
|
|
1446
|
+
not isinstance(stale_reasons, list)
|
|
1447
|
+
or any(reason not in NATIVE_CHECK_STALE_REASONS for reason in stale_reasons)
|
|
1448
|
+
or len(stale_reasons) != len(set(stale_reasons))
|
|
1449
|
+
or stale_reasons
|
|
1450
|
+
!= [reason for reason in NATIVE_CHECK_STALE_REASONS if reason in stale_reasons]
|
|
1451
|
+
):
|
|
1452
|
+
raise NativeEvidenceError("Native check receipt stale reasons are not canonical")
|
|
1453
|
+
if not isinstance(receipt["stale"], bool) or receipt["stale"] != bool(stale_reasons):
|
|
1454
|
+
raise NativeEvidenceError("Native check receipt stale state is inconsistent")
|
|
1455
|
+
expected_status = (
|
|
1456
|
+
"passed" if parsed_counts["issueCount"] == 0 and not receipt["stale"] else "failed"
|
|
1457
|
+
)
|
|
1458
|
+
if receipt["status"] != expected_status:
|
|
1459
|
+
raise NativeEvidenceError("Native check receipt status is inconsistent")
|
|
1460
|
+
if receipt["status"] == "passed" and (
|
|
1461
|
+
parsed_counts["filesSelected"] > NATIVE_CHECK_LIMITS["maxFiles"]
|
|
1462
|
+
or parsed_counts["filesScanned"] + parsed_counts["binaryFilesSkipped"]
|
|
1463
|
+
!= parsed_counts["filesSelected"]
|
|
1464
|
+
):
|
|
1465
|
+
raise NativeEvidenceError("Native check receipt passed without full file coverage")
|
|
1466
|
+
if parsed_counts["filesSelected"] > NATIVE_CHECK_LIMITS["maxFiles"] and not any(
|
|
1467
|
+
issue["kind"] == "scan-limit" for issue in issues
|
|
1468
|
+
):
|
|
1469
|
+
raise NativeEvidenceError("Native check receipt exceeded its budget without an issue")
|
|
1470
|
+
|
|
1471
|
+
content = {
|
|
1472
|
+
"schema": "comet.native.check-receipt.v1",
|
|
1473
|
+
"change": change,
|
|
1474
|
+
"sourceRevision": source_revision,
|
|
1475
|
+
"checker": {
|
|
1476
|
+
"policy": "scoped-text-safety",
|
|
1477
|
+
"version": 1,
|
|
1478
|
+
"hash": NATIVE_CHECKER_HASH,
|
|
1479
|
+
"limits": dict(NATIVE_CHECK_LIMITS),
|
|
1480
|
+
},
|
|
1481
|
+
"inputHash": expected_input_hash,
|
|
1482
|
+
"status": receipt["status"],
|
|
1483
|
+
"startedAt": started_at,
|
|
1484
|
+
"endedAt": ended_at,
|
|
1485
|
+
"contract": parsed_contract,
|
|
1486
|
+
"implementation": parsed_implementation,
|
|
1487
|
+
"counts": parsed_counts,
|
|
1488
|
+
"issues": issues,
|
|
1489
|
+
"issuesTruncated": receipt["issuesTruncated"],
|
|
1490
|
+
"stale": receipt["stale"],
|
|
1491
|
+
"staleReasons": stale_reasons,
|
|
1492
|
+
}
|
|
1493
|
+
receipt_hash = _hash(receipt["receiptHash"], "Native check receipt content hash")
|
|
1494
|
+
if (
|
|
1495
|
+
receipt_hash != match.group(1)
|
|
1496
|
+
or canonical_hash("comet.native.check-receipt.v1", content) != receipt_hash
|
|
1497
|
+
):
|
|
1498
|
+
raise NativeEvidenceError("Native check receipt filename/content hash mismatch")
|
|
1499
|
+
|
|
1500
|
+
selected_files = [entry for entry in scope["changes"] if entry["after"] is not None]
|
|
1501
|
+
selected_bytes = sum(entry["after"]["size"] for entry in selected_files)
|
|
1502
|
+
if (
|
|
1503
|
+
receipt["stale"]
|
|
1504
|
+
or change != expected_change
|
|
1505
|
+
or source_revision != expected_source_revision
|
|
1506
|
+
or set(parsed_contract.values()) != {expected_contract_hash}
|
|
1507
|
+
or parsed_implementation["scopeHash"] != scope["scopeHash"]
|
|
1508
|
+
or set(
|
|
1509
|
+
(
|
|
1510
|
+
parsed_implementation["expectedSnapshotHash"],
|
|
1511
|
+
parsed_implementation["beforeSnapshotHash"],
|
|
1512
|
+
parsed_implementation["afterSnapshotHash"],
|
|
1513
|
+
)
|
|
1514
|
+
)
|
|
1515
|
+
!= {scope["currentProjectionHash"]}
|
|
1516
|
+
or parsed_counts["filesSelected"] != len(selected_files)
|
|
1517
|
+
or (
|
|
1518
|
+
receipt["status"] == "passed"
|
|
1519
|
+
and (
|
|
1520
|
+
parsed_counts["filesScanned"] + parsed_counts["binaryFilesSkipped"]
|
|
1521
|
+
!= len(selected_files)
|
|
1522
|
+
or parsed_counts["bytesScanned"] != selected_bytes
|
|
1523
|
+
)
|
|
1524
|
+
)
|
|
1525
|
+
):
|
|
1526
|
+
raise NativeEvidenceError("Native check receipt is not bound to the verification facts")
|
|
1527
|
+
if expected_result == "pass" and receipt["status"] != "passed":
|
|
1528
|
+
raise NativeEvidenceError("Native check receipt outcome does not support a pass")
|
|
1529
|
+
return {**content, "receiptHash": receipt_hash}
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
def parse_verification_bundle(
|
|
1533
|
+
*,
|
|
1534
|
+
project_root: Path,
|
|
1535
|
+
change_root: Path,
|
|
1536
|
+
evidence_ref: Any,
|
|
1537
|
+
state: dict[str, Any],
|
|
1538
|
+
expected_result: str | None = None,
|
|
1539
|
+
expected_freshness: str | None = None,
|
|
1540
|
+
verify_current_files: bool = False,
|
|
1541
|
+
) -> dict[str, Any]:
|
|
1542
|
+
change = state.get("name")
|
|
1543
|
+
if not isinstance(change, str) or not CHANGE_NAME_PATTERN.fullmatch(change):
|
|
1544
|
+
raise NativeEvidenceError("Native change name is invalid")
|
|
1545
|
+
reference = portable_ref(evidence_ref, "Verification evidence ref")
|
|
1546
|
+
match = re.fullmatch(r"runtime/evidence/verifications/([a-f0-9]{64})\.json", reference)
|
|
1547
|
+
if not match:
|
|
1548
|
+
raise NativeEvidenceError("Verification evidence ref is not content addressed")
|
|
1549
|
+
envelope = _record(
|
|
1550
|
+
read_contained_json(change_root, reference, "Verification envelope"),
|
|
1551
|
+
"Verification envelope",
|
|
1552
|
+
)
|
|
1553
|
+
_exact_keys(
|
|
1554
|
+
envelope,
|
|
1555
|
+
[
|
|
1556
|
+
"schema",
|
|
1557
|
+
"change",
|
|
1558
|
+
"sourceRevision",
|
|
1559
|
+
"result",
|
|
1560
|
+
"freshness",
|
|
1561
|
+
"contractHash",
|
|
1562
|
+
"acceptanceCriteriaHash",
|
|
1563
|
+
"implementationScopeRef",
|
|
1564
|
+
"implementationScopeHash",
|
|
1565
|
+
"reportRef",
|
|
1566
|
+
"reportHash",
|
|
1567
|
+
"acceptanceTrace",
|
|
1568
|
+
"partialAllowanceRef",
|
|
1569
|
+
"partialAllowanceHash",
|
|
1570
|
+
"receiptRef",
|
|
1571
|
+
"createdAt",
|
|
1572
|
+
"envelopeHash",
|
|
1573
|
+
],
|
|
1574
|
+
label="Verification envelope",
|
|
1575
|
+
)
|
|
1576
|
+
if (
|
|
1577
|
+
envelope["schema"] != "comet.native.verification-evidence.v1"
|
|
1578
|
+
or envelope["change"] != change
|
|
1579
|
+
):
|
|
1580
|
+
raise NativeEvidenceError("Verification envelope schema/change binding is invalid")
|
|
1581
|
+
_positive_int(envelope["sourceRevision"], "Verification source revision")
|
|
1582
|
+
if envelope["result"] not in {"pass", "fail"} or envelope["freshness"] not in {
|
|
1583
|
+
"complete",
|
|
1584
|
+
"partial",
|
|
1585
|
+
}:
|
|
1586
|
+
raise NativeEvidenceError("Verification result/freshness is invalid")
|
|
1587
|
+
if expected_result is not None and envelope["result"] != expected_result:
|
|
1588
|
+
raise NativeEvidenceError("Verification envelope has the wrong result")
|
|
1589
|
+
if expected_freshness is not None and envelope["freshness"] != expected_freshness:
|
|
1590
|
+
raise NativeEvidenceError("Verification envelope has the wrong freshness")
|
|
1591
|
+
contract = build_contract_from_change(change_root, state)
|
|
1592
|
+
contract_hash = _hash(envelope["contractHash"], "Verification contract hash")
|
|
1593
|
+
acceptance_hash = _hash(envelope["acceptanceCriteriaHash"], "Verification acceptance hash")
|
|
1594
|
+
if contract_hash != contract["contractHash"] or acceptance_hash != contract["acceptanceHash"]:
|
|
1595
|
+
raise NativeEvidenceError("Verification envelope is not bound to the archived contract")
|
|
1596
|
+
scope_bundle = parse_scope_bundle(
|
|
1597
|
+
change_root,
|
|
1598
|
+
envelope["implementationScopeRef"],
|
|
1599
|
+
project_root=project_root if verify_current_files else None,
|
|
1600
|
+
)
|
|
1601
|
+
scope = scope_bundle["scope"]
|
|
1602
|
+
if (
|
|
1603
|
+
envelope["implementationScopeHash"] != scope["scopeHash"]
|
|
1604
|
+
or scope["contractHash"] != contract_hash
|
|
1605
|
+
or envelope["freshness"] != ("complete" if scope["complete"] else "partial")
|
|
1606
|
+
):
|
|
1607
|
+
raise NativeEvidenceError("Verification envelope/scope binding is invalid")
|
|
1608
|
+
_parse_acceptance_trace(
|
|
1609
|
+
envelope["acceptanceTrace"], contract=contract, project_root=project_root
|
|
1610
|
+
)
|
|
1611
|
+
report_ref = portable_ref(envelope["reportRef"], "Verification report ref")
|
|
1612
|
+
report_hash = _hash(envelope["reportHash"], "Verification report hash")
|
|
1613
|
+
report_snapshot = read_contained_json(
|
|
1614
|
+
change_root,
|
|
1615
|
+
f"runtime/evidence/reports/{report_hash}.json",
|
|
1616
|
+
"Verification report snapshot",
|
|
1617
|
+
)
|
|
1618
|
+
if (
|
|
1619
|
+
not isinstance(report_snapshot, dict)
|
|
1620
|
+
or set(report_snapshot) != {"schema", "reportHash", "content"}
|
|
1621
|
+
or report_snapshot.get("schema") != "comet.native.verification-report.v1"
|
|
1622
|
+
or report_snapshot.get("reportHash") != report_hash
|
|
1623
|
+
or not isinstance(report_snapshot.get("content"), str)
|
|
1624
|
+
or hashlib.sha256(report_snapshot["content"].encode()).hexdigest() != report_hash
|
|
1625
|
+
):
|
|
1626
|
+
raise NativeEvidenceError("Verification report snapshot hash mismatch")
|
|
1627
|
+
report_text = report_snapshot["content"]
|
|
1628
|
+
if verify_current_files:
|
|
1629
|
+
report = contained_file(change_root, report_ref, "Verification report")
|
|
1630
|
+
if sha256_file(report) != report_hash:
|
|
1631
|
+
raise NativeEvidenceError("Verification report content hash mismatch")
|
|
1632
|
+
for criterion in contract["acceptance"]:
|
|
1633
|
+
if criterion["id"] not in report_text:
|
|
1634
|
+
raise NativeEvidenceError("Verification report does not preserve every acceptance ID")
|
|
1635
|
+
allowance_ref = envelope["partialAllowanceRef"]
|
|
1636
|
+
allowance_hash = envelope["partialAllowanceHash"]
|
|
1637
|
+
if envelope["freshness"] == "complete":
|
|
1638
|
+
if allowance_ref is not None or allowance_hash is not None:
|
|
1639
|
+
raise NativeEvidenceError("Complete evidence unexpectedly has a partial allowance")
|
|
1640
|
+
else:
|
|
1641
|
+
allowance = parse_partial_allowance(change_root, allowance_ref, expected_change=change)
|
|
1642
|
+
if (
|
|
1643
|
+
allowance_hash != allowance["allowanceHash"]
|
|
1644
|
+
or allowance["scopeHash"] != scope["scopeHash"]
|
|
1645
|
+
or allowance["sourceRevision"] >= envelope["sourceRevision"]
|
|
1646
|
+
):
|
|
1647
|
+
raise NativeEvidenceError("Verification envelope/allowance binding is invalid")
|
|
1648
|
+
receipt_ref = envelope["receiptRef"]
|
|
1649
|
+
if receipt_ref is not None:
|
|
1650
|
+
_parse_check_receipt(
|
|
1651
|
+
change_root=change_root,
|
|
1652
|
+
receipt_ref=receipt_ref,
|
|
1653
|
+
expected_change=change,
|
|
1654
|
+
expected_source_revision=envelope["sourceRevision"],
|
|
1655
|
+
expected_result=envelope["result"],
|
|
1656
|
+
expected_contract_hash=contract_hash,
|
|
1657
|
+
scope=scope,
|
|
1658
|
+
)
|
|
1659
|
+
_timestamp(envelope["createdAt"], "Verification timestamp")
|
|
1660
|
+
content = {key: value for key, value in envelope.items() if key != "envelopeHash"}
|
|
1661
|
+
envelope_hash = _hash(envelope["envelopeHash"], "Verification envelope hash")
|
|
1662
|
+
if (
|
|
1663
|
+
envelope_hash != match.group(1)
|
|
1664
|
+
or canonical_hash("comet.native.verification-evidence.v1", content) != envelope_hash
|
|
1665
|
+
):
|
|
1666
|
+
raise NativeEvidenceError("Verification envelope filename/content hash mismatch")
|
|
1667
|
+
return {"envelope": envelope, "scope": scope, "contract": contract}
|
|
1668
|
+
|
|
1669
|
+
|
|
1670
|
+
def check_archive_transaction(
|
|
1671
|
+
workspace: Path,
|
|
1672
|
+
commit_data: Any,
|
|
1673
|
+
expected_change: str,
|
|
1674
|
+
expected_preflight_hash: str,
|
|
1675
|
+
) -> dict[str, str]:
|
|
1676
|
+
"""Bind a successful Archive envelope to its durable v2 transaction journal."""
|
|
1677
|
+
check = "archive_transaction"
|
|
1678
|
+
try:
|
|
1679
|
+
commit = _record(commit_data, "Archive commit data")
|
|
1680
|
+
transaction_id = commit.get("transactionId")
|
|
1681
|
+
if not isinstance(transaction_id, str) or not re.fullmatch(
|
|
1682
|
+
r"[a-f0-9-]{8,}", transaction_id
|
|
1683
|
+
):
|
|
1684
|
+
raise NativeEvidenceError("Archive commit has no valid transactionId")
|
|
1685
|
+
if commit.get("preflightHash") != expected_preflight_hash:
|
|
1686
|
+
raise NativeEvidenceError("Archive commit does not match the expected preflight hash")
|
|
1687
|
+
native_root = workspace / "docs" / "comet"
|
|
1688
|
+
journal_ref = f"runtime/transactions/{transaction_id}/transaction.json"
|
|
1689
|
+
journal = _record(
|
|
1690
|
+
read_contained_json(native_root, journal_ref, "Archive transaction journal"),
|
|
1691
|
+
"Archive transaction journal",
|
|
1692
|
+
)
|
|
1693
|
+
_exact_keys(
|
|
1694
|
+
journal,
|
|
1695
|
+
[
|
|
1696
|
+
"schema",
|
|
1697
|
+
"id",
|
|
1698
|
+
"kind",
|
|
1699
|
+
"status",
|
|
1700
|
+
"change",
|
|
1701
|
+
"createdAt",
|
|
1702
|
+
"preflightHash",
|
|
1703
|
+
"operations",
|
|
1704
|
+
],
|
|
1705
|
+
label="Archive transaction journal",
|
|
1706
|
+
)
|
|
1707
|
+
if (
|
|
1708
|
+
journal["schema"] != "comet.native.transaction.v2"
|
|
1709
|
+
or journal["id"] != transaction_id
|
|
1710
|
+
or journal["kind"] != "archive"
|
|
1711
|
+
or journal["status"] != "committed"
|
|
1712
|
+
or journal["change"] != expected_change
|
|
1713
|
+
or journal["preflightHash"] != expected_preflight_hash
|
|
1714
|
+
):
|
|
1715
|
+
raise NativeEvidenceError("Archive envelope is not bound to one committed v2 journal")
|
|
1716
|
+
_timestamp(journal["createdAt"], "Archive transaction timestamp")
|
|
1717
|
+
operations = journal["operations"]
|
|
1718
|
+
if not isinstance(operations, list) or not operations or len(operations) > 65:
|
|
1719
|
+
raise NativeEvidenceError("Archive transaction operations are invalid")
|
|
1720
|
+
operation_ids: list[str] = []
|
|
1721
|
+
for index, raw in enumerate(operations):
|
|
1722
|
+
operation = _record(raw, f"Archive transaction operation {index}")
|
|
1723
|
+
_exact_keys(
|
|
1724
|
+
operation,
|
|
1725
|
+
["id", "type", "target", "expectedTargetHash"],
|
|
1726
|
+
["source", "staged", "backup", "expectedSourceHash", "stagedHash"],
|
|
1727
|
+
label=f"Archive transaction operation {index}",
|
|
1728
|
+
)
|
|
1729
|
+
operation_id = operation["id"]
|
|
1730
|
+
if not isinstance(operation_id, str) or not re.fullmatch(
|
|
1731
|
+
r"[a-z0-9][a-z0-9-]*", operation_id
|
|
1732
|
+
):
|
|
1733
|
+
raise NativeEvidenceError(f"Archive transaction operation {index} id is invalid")
|
|
1734
|
+
operation_ids.append(operation_id)
|
|
1735
|
+
portable_ref(operation["target"], f"Archive transaction operation {index} target")
|
|
1736
|
+
for field in ("source", "staged", "backup"):
|
|
1737
|
+
if field in operation:
|
|
1738
|
+
portable_ref(operation[field], f"Archive transaction operation {index} {field}")
|
|
1739
|
+
if operation["expectedTargetHash"] is not None:
|
|
1740
|
+
_hash(
|
|
1741
|
+
operation["expectedTargetHash"],
|
|
1742
|
+
f"Archive transaction operation {index} target hash",
|
|
1743
|
+
)
|
|
1744
|
+
if operation["type"] == "write":
|
|
1745
|
+
if (
|
|
1746
|
+
"staged" not in operation
|
|
1747
|
+
or "source" in operation
|
|
1748
|
+
or "expectedSourceHash" in operation
|
|
1749
|
+
):
|
|
1750
|
+
raise NativeEvidenceError("Archive write operation shape is invalid")
|
|
1751
|
+
_hash(operation.get("stagedHash"), "Archive staged hash")
|
|
1752
|
+
if (operation["expectedTargetHash"] is None) != ("backup" not in operation):
|
|
1753
|
+
raise NativeEvidenceError("Archive write backup binding is invalid")
|
|
1754
|
+
elif operation["type"] == "remove":
|
|
1755
|
+
if (
|
|
1756
|
+
any(
|
|
1757
|
+
field in operation
|
|
1758
|
+
for field in ("source", "staged", "stagedHash", "expectedSourceHash")
|
|
1759
|
+
)
|
|
1760
|
+
or "backup" not in operation
|
|
1761
|
+
or operation["expectedTargetHash"] is None
|
|
1762
|
+
):
|
|
1763
|
+
raise NativeEvidenceError("Archive remove operation shape is invalid")
|
|
1764
|
+
elif operation["type"] == "move":
|
|
1765
|
+
if (
|
|
1766
|
+
"source" not in operation
|
|
1767
|
+
or any(field in operation for field in ("staged", "stagedHash", "backup"))
|
|
1768
|
+
or operation["expectedTargetHash"] is not None
|
|
1769
|
+
):
|
|
1770
|
+
raise NativeEvidenceError("Archive move operation shape is invalid")
|
|
1771
|
+
_hash(operation.get("expectedSourceHash"), "Archive move source hash")
|
|
1772
|
+
else:
|
|
1773
|
+
raise NativeEvidenceError("Archive transaction operation type is invalid")
|
|
1774
|
+
if len(set(operation_ids)) != len(operation_ids):
|
|
1775
|
+
raise NativeEvidenceError("Archive transaction operation IDs are not unique")
|
|
1776
|
+
move = operations[-1]
|
|
1777
|
+
target = move.get("target")
|
|
1778
|
+
if (
|
|
1779
|
+
move.get("id") != "archive-change"
|
|
1780
|
+
or move.get("type") != "move"
|
|
1781
|
+
or move.get("source") != f"changes/{expected_change}"
|
|
1782
|
+
or not isinstance(target, str)
|
|
1783
|
+
or not re.fullmatch(
|
|
1784
|
+
rf"archive/\d{{4}}-\d{{2}}-\d{{2}}-{re.escape(expected_change)}", target
|
|
1785
|
+
)
|
|
1786
|
+
):
|
|
1787
|
+
raise NativeEvidenceError(
|
|
1788
|
+
"Committed Archive journal has no exact content-bound final move"
|
|
1789
|
+
)
|
|
1790
|
+
for operation in operations[:-1]:
|
|
1791
|
+
if operation["type"] == "move" or not re.fullmatch(
|
|
1792
|
+
r"specs/[a-z][a-z0-9]*(?:-[a-z0-9]+)*/spec\.md", operation["target"]
|
|
1793
|
+
):
|
|
1794
|
+
raise NativeEvidenceError("Archive transaction has a non-canonical spec operation")
|
|
1795
|
+
archive_dir = native_root.joinpath(*PurePosixPath(target).parts)
|
|
1796
|
+
_assert_real_directory_chain(native_root, archive_dir, "Archived change directory")
|
|
1797
|
+
if archive_dir.is_symlink() or not archive_dir.is_dir():
|
|
1798
|
+
raise NativeEvidenceError(
|
|
1799
|
+
"Archive transaction target is not the live archive directory"
|
|
1800
|
+
)
|
|
1801
|
+
events_path = contained_file(
|
|
1802
|
+
native_root,
|
|
1803
|
+
f"runtime/transactions/{transaction_id}/events.jsonl",
|
|
1804
|
+
"Archive transaction events",
|
|
1805
|
+
)
|
|
1806
|
+
events = []
|
|
1807
|
+
for line_number, line in enumerate(events_path.read_text(encoding="utf-8").splitlines(), 1):
|
|
1808
|
+
if not line:
|
|
1809
|
+
raise NativeEvidenceError("Archive transaction events contain a blank line")
|
|
1810
|
+
event = _record(json.loads(line), f"Archive transaction event {line_number}")
|
|
1811
|
+
_exact_keys(
|
|
1812
|
+
event,
|
|
1813
|
+
["sequence", "timestamp", "type"],
|
|
1814
|
+
["operationId"],
|
|
1815
|
+
label=f"Archive transaction event {line_number}",
|
|
1816
|
+
)
|
|
1817
|
+
if event["sequence"] != line_number:
|
|
1818
|
+
raise NativeEvidenceError("Archive transaction event sequence is invalid")
|
|
1819
|
+
_timestamp(event["timestamp"], f"Archive transaction event {line_number} timestamp")
|
|
1820
|
+
events.append(event)
|
|
1821
|
+
expected_events: list[tuple[str, str | None]] = [("prepared", None)]
|
|
1822
|
+
for operation in operations:
|
|
1823
|
+
expected_events.extend(
|
|
1824
|
+
[("operation-started", operation["id"]), ("operation-completed", operation["id"])]
|
|
1825
|
+
)
|
|
1826
|
+
expected_events.extend(
|
|
1827
|
+
[("archive-finalization-started", None), ("archive-finalized", None), ("commit", None)]
|
|
1828
|
+
)
|
|
1829
|
+
actual_events = [(event["type"], event.get("operationId")) for event in events]
|
|
1830
|
+
if actual_events != expected_events:
|
|
1831
|
+
raise NativeEvidenceError(
|
|
1832
|
+
"Archive transaction event lifecycle is incomplete or reordered"
|
|
1833
|
+
)
|
|
1834
|
+
serialized = canonical_json(journal)
|
|
1835
|
+
if str(workspace) in serialized or "\\workspace" in serialized:
|
|
1836
|
+
raise NativeEvidenceError(
|
|
1837
|
+
"Archive transaction journal exposed an absolute workspace path"
|
|
1838
|
+
)
|
|
1839
|
+
except (NativeEvidenceError, OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
1840
|
+
return failed(check, str(error))
|
|
1841
|
+
return passed(check)
|
|
1842
|
+
|
|
1843
|
+
|
|
1844
|
+
def check_checkpoint_cas_envelopes(paths: Iterable[Path]) -> dict[str, str]:
|
|
1845
|
+
"""Require two raw checkpoint envelopes from one revision with one winner."""
|
|
1846
|
+
check = "cas_single_winner"
|
|
1847
|
+
evidence_paths = list(paths)
|
|
1848
|
+
if len(evidence_paths) != 2:
|
|
1849
|
+
return failed(
|
|
1850
|
+
check, f"Expected exactly two checkpoint envelopes, found {len(evidence_paths)}"
|
|
1851
|
+
)
|
|
1852
|
+
envelopes: list[dict[str, Any]] = []
|
|
1853
|
+
for path in evidence_paths:
|
|
1854
|
+
if not path.is_file():
|
|
1855
|
+
return failed(check, f"Missing checkpoint evidence file: {path}")
|
|
1856
|
+
try:
|
|
1857
|
+
payload = read_json(path)
|
|
1858
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
1859
|
+
return failed(check, f"Invalid checkpoint evidence in {path.name}: {error}")
|
|
1860
|
+
if not isinstance(payload, dict) or payload.get("command") != "checkpoint":
|
|
1861
|
+
return failed(check, f"{path.name} is not a raw Native checkpoint envelope")
|
|
1862
|
+
if not isinstance(payload.get("data"), dict):
|
|
1863
|
+
return failed(check, f"{path.name} has no structured checkpoint data")
|
|
1864
|
+
envelopes.append(payload)
|
|
1865
|
+
|
|
1866
|
+
winners = [item for item in envelopes if item.get("exitCode") == 0]
|
|
1867
|
+
conflicts = [item for item in envelopes if item.get("exitCode") == 73]
|
|
1868
|
+
if len(winners) != 1 or len(conflicts) != 1:
|
|
1869
|
+
return failed(
|
|
1870
|
+
check,
|
|
1871
|
+
f"Expected one exit 0 and one exit 73; observed {[item.get('exitCode') for item in envelopes]}",
|
|
1872
|
+
)
|
|
1873
|
+
|
|
1874
|
+
winner = winners[0]["data"]
|
|
1875
|
+
conflict = conflicts[0]["data"]
|
|
1876
|
+
expected = winner.get("expectedRevision")
|
|
1877
|
+
winner_change = winner.get("change")
|
|
1878
|
+
if not isinstance(expected, int) or isinstance(expected, bool) or expected < 1:
|
|
1879
|
+
return failed(check, "Checkpoint winner has no positive expectedRevision")
|
|
1880
|
+
if (
|
|
1881
|
+
winner.get("outcome") != "recorded"
|
|
1882
|
+
or winner.get("previousRevision") != expected
|
|
1883
|
+
or winner.get("revision") != expected + 1
|
|
1884
|
+
or not isinstance(winner_change, dict)
|
|
1885
|
+
or not isinstance(winner_change.get("name"), str)
|
|
1886
|
+
or winner_change.get("revision") != expected + 1
|
|
1887
|
+
):
|
|
1888
|
+
return failed(check, f"Checkpoint winner has inconsistent CAS data: {winner!r}")
|
|
1889
|
+
if (
|
|
1890
|
+
conflict.get("outcome") != "revision-conflict"
|
|
1891
|
+
or conflict.get("change") != winner_change.get("name")
|
|
1892
|
+
or conflict.get("expectedRevision") != expected
|
|
1893
|
+
or conflict.get("actualRevision") != expected + 1
|
|
1894
|
+
):
|
|
1895
|
+
return failed(check, f"Checkpoint conflict has inconsistent CAS data: {conflict!r}")
|
|
1896
|
+
return passed(check)
|
|
1897
|
+
|
|
1898
|
+
|
|
1899
|
+
def _get_first(mapping: dict[str, Any], *keys: str) -> Any:
|
|
1900
|
+
for key in keys:
|
|
1901
|
+
if key in mapping:
|
|
1902
|
+
return mapping[key]
|
|
1903
|
+
return None
|
|
1904
|
+
|
|
1905
|
+
|
|
1906
|
+
def _normalise_projection(value: Any) -> dict[str, Any] | None:
|
|
1907
|
+
if not isinstance(value, dict):
|
|
1908
|
+
return None
|
|
1909
|
+
name = _get_first(value, "name", "change", "changeName", "change_name")
|
|
1910
|
+
phase = _get_first(value, "phase", "currentPhase", "current_phase")
|
|
1911
|
+
if not isinstance(name, str) or phase is None:
|
|
1912
|
+
return None
|
|
1913
|
+
|
|
1914
|
+
verification = _get_first(value, "verificationResult", "verification_result")
|
|
1915
|
+
if verification is None and isinstance(value.get("verification"), dict):
|
|
1916
|
+
verification = _get_first(value["verification"], "result", "status")
|
|
1917
|
+
next_command = _get_first(
|
|
1918
|
+
value,
|
|
1919
|
+
"nextCommand",
|
|
1920
|
+
"next_command",
|
|
1921
|
+
"nextAction",
|
|
1922
|
+
"next_action",
|
|
1923
|
+
"next",
|
|
1924
|
+
)
|
|
1925
|
+
finding_source = value.get("findingSummary") or value.get("finding_summary")
|
|
1926
|
+
if not isinstance(finding_source, dict):
|
|
1927
|
+
finding_source = value.get("findings") if isinstance(value.get("findings"), dict) else {}
|
|
1928
|
+
finding_codes = finding_source.get("codes")
|
|
1929
|
+
if not isinstance(finding_codes, list):
|
|
1930
|
+
finding_codes = []
|
|
1931
|
+
continuation = value.get("continuation")
|
|
1932
|
+
continuation_projection = None
|
|
1933
|
+
if isinstance(continuation, dict):
|
|
1934
|
+
required_inputs = continuation.get(
|
|
1935
|
+
"requiredInputs", continuation.get("required_inputs", [])
|
|
1936
|
+
)
|
|
1937
|
+
continuation_projection = {
|
|
1938
|
+
"disposition": continuation.get("disposition"),
|
|
1939
|
+
"action": continuation.get("action"),
|
|
1940
|
+
"command": continuation.get("command"),
|
|
1941
|
+
"requiresUserDecision": continuation.get(
|
|
1942
|
+
"requiresUserDecision", continuation.get("requires_user_decision")
|
|
1943
|
+
),
|
|
1944
|
+
"requiredInputs": sorted(required_inputs) if isinstance(required_inputs, list) else [],
|
|
1945
|
+
"requiredInputsTruncated": continuation.get("requiredInputsTruncated", False)
|
|
1946
|
+
is True,
|
|
1947
|
+
}
|
|
1948
|
+
finding_projection = {
|
|
1949
|
+
"total": finding_source.get("total", 0),
|
|
1950
|
+
"errors": finding_source.get("errors", 0),
|
|
1951
|
+
"warnings": finding_source.get("warnings", 0),
|
|
1952
|
+
"info": finding_source.get("info", 0),
|
|
1953
|
+
"requiresUserDecision": finding_source.get(
|
|
1954
|
+
"requiresUserDecision", finding_source.get("requires_user_decision", False)
|
|
1955
|
+
)
|
|
1956
|
+
is True,
|
|
1957
|
+
"codes": sorted(item for item in finding_codes if isinstance(item, str)),
|
|
1958
|
+
"truncated": finding_source.get("truncated", False) is True,
|
|
1959
|
+
}
|
|
1960
|
+
return {
|
|
1961
|
+
"name": name,
|
|
1962
|
+
"phase": phase,
|
|
1963
|
+
"revision": value.get("revision"),
|
|
1964
|
+
"selected": value.get("selected", False) is True,
|
|
1965
|
+
"nextCommand": next_command,
|
|
1966
|
+
"verificationResult": verification,
|
|
1967
|
+
"archiveReady": value.get("archiveReady", value.get("archive_ready")),
|
|
1968
|
+
"findings": finding_projection,
|
|
1969
|
+
"continuation": continuation_projection,
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
|
|
1973
|
+
def _projection_records(payload: Any, *, dashboard: bool) -> list[dict[str, Any]]:
|
|
1974
|
+
if not isinstance(payload, dict):
|
|
1975
|
+
return []
|
|
1976
|
+
root = payload.get("data", payload)
|
|
1977
|
+
candidates: Any = None
|
|
1978
|
+
if dashboard:
|
|
1979
|
+
if isinstance(root, dict) and isinstance(root.get("native"), dict):
|
|
1980
|
+
candidates = root["native"].get("changes")
|
|
1981
|
+
elif isinstance(root, dict) and isinstance(root.get("snapshot"), dict):
|
|
1982
|
+
snapshot = root["snapshot"]
|
|
1983
|
+
if isinstance(snapshot.get("native"), dict):
|
|
1984
|
+
candidates = snapshot["native"].get("changes")
|
|
1985
|
+
else:
|
|
1986
|
+
candidates = snapshot.get("changes")
|
|
1987
|
+
elif isinstance(root, dict):
|
|
1988
|
+
candidates = root.get("changes")
|
|
1989
|
+
elif isinstance(root, dict) and root.get("schema") == "comet.native.status-page.v1":
|
|
1990
|
+
candidates = root.get("items")
|
|
1991
|
+
elif isinstance(root, list):
|
|
1992
|
+
candidates = root
|
|
1993
|
+
else:
|
|
1994
|
+
candidates = [root]
|
|
1995
|
+
if not isinstance(candidates, list):
|
|
1996
|
+
return []
|
|
1997
|
+
result = []
|
|
1998
|
+
for candidate in candidates:
|
|
1999
|
+
projection = _normalise_projection(candidate)
|
|
2000
|
+
if projection is not None:
|
|
2001
|
+
result.append(projection)
|
|
2002
|
+
return result
|
|
2003
|
+
|
|
2004
|
+
|
|
2005
|
+
def check_dashboard_projection(
|
|
2006
|
+
cli_path: Path,
|
|
2007
|
+
dashboard_path: Path,
|
|
2008
|
+
*,
|
|
2009
|
+
comparison_dashboard: bool = True,
|
|
2010
|
+
) -> dict[str, str]:
|
|
2011
|
+
"""Require Dashboard's Native projection to match the CLI projection."""
|
|
2012
|
+
check = "dashboard_projection"
|
|
2013
|
+
missing = [str(path) for path in (cli_path, dashboard_path) if not path.is_file()]
|
|
2014
|
+
if missing:
|
|
2015
|
+
return failed(check, f"Missing projection evidence: {', '.join(missing)}")
|
|
2016
|
+
try:
|
|
2017
|
+
cli = _projection_records(read_json(cli_path), dashboard=False)
|
|
2018
|
+
dashboard = _projection_records(
|
|
2019
|
+
read_json(dashboard_path), dashboard=comparison_dashboard
|
|
2020
|
+
)
|
|
2021
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
2022
|
+
return failed(check, f"Invalid projection evidence: {error}")
|
|
2023
|
+
if not cli:
|
|
2024
|
+
return failed(check, "CLI evidence has no Native change projection")
|
|
2025
|
+
if not dashboard:
|
|
2026
|
+
return failed(check, "Dashboard evidence has no Native change projection")
|
|
2027
|
+
|
|
2028
|
+
if len({item["name"] for item in cli}) != len(cli):
|
|
2029
|
+
return failed(check, "CLI evidence repeats a Native change projection")
|
|
2030
|
+
if len({item["name"] for item in dashboard}) != len(dashboard):
|
|
2031
|
+
return failed(check, "Dashboard evidence repeats a Native change projection")
|
|
2032
|
+
dashboard_by_name = {item["name"]: item for item in dashboard}
|
|
2033
|
+
cli_names = {item["name"] for item in cli}
|
|
2034
|
+
dashboard_names = set(dashboard_by_name)
|
|
2035
|
+
mismatches = []
|
|
2036
|
+
for cli_item in cli:
|
|
2037
|
+
dashboard_item = dashboard_by_name.get(cli_item["name"])
|
|
2038
|
+
if dashboard_item is None:
|
|
2039
|
+
mismatches.append(f"{cli_item['name']}: missing from Dashboard")
|
|
2040
|
+
elif dashboard_item != cli_item:
|
|
2041
|
+
mismatches.append(f"{cli_item['name']}: CLI={cli_item!r}, Dashboard={dashboard_item!r}")
|
|
2042
|
+
for extra in sorted(dashboard_names - cli_names):
|
|
2043
|
+
mismatches.append(f"{extra}: unexpected Dashboard-only change")
|
|
2044
|
+
if mismatches:
|
|
2045
|
+
return failed(check, "; ".join(mismatches))
|
|
2046
|
+
return passed(check)
|
|
2047
|
+
|
|
2048
|
+
|
|
2049
|
+
def active_changes(workspace: Path) -> list[Path]:
|
|
2050
|
+
root = workspace / "docs" / "comet" / "changes"
|
|
2051
|
+
if not root.exists():
|
|
2052
|
+
return []
|
|
2053
|
+
_assert_real_directory_chain(workspace, root, "Native active changes root")
|
|
2054
|
+
return sorted(path for path in root.iterdir() if path.is_dir() and not path.is_symlink())
|
|
2055
|
+
|
|
2056
|
+
|
|
2057
|
+
def archive_changes(workspace: Path) -> list[Path]:
|
|
2058
|
+
root = workspace / "docs" / "comet" / "archive"
|
|
2059
|
+
if not root.exists():
|
|
2060
|
+
return []
|
|
2061
|
+
_assert_real_directory_chain(workspace, root, "Native archive root")
|
|
2062
|
+
return sorted(
|
|
2063
|
+
path
|
|
2064
|
+
for path in root.iterdir()
|
|
2065
|
+
if path.is_dir() and not path.is_symlink() and "-" in path.name
|
|
2066
|
+
)
|
|
2067
|
+
|
|
2068
|
+
|
|
2069
|
+
def check_pytest(workspace: Path, check: str = "project_tests") -> dict[str, str]:
|
|
2070
|
+
try:
|
|
2071
|
+
result = subprocess.run(
|
|
2072
|
+
[sys.executable, "-m", "pytest", "-q"],
|
|
2073
|
+
cwd=workspace,
|
|
2074
|
+
capture_output=True,
|
|
2075
|
+
text=True,
|
|
2076
|
+
timeout=30,
|
|
2077
|
+
check=False,
|
|
2078
|
+
)
|
|
2079
|
+
except Exception as error:
|
|
2080
|
+
return failed(check, str(error))
|
|
2081
|
+
if result.returncode != 0:
|
|
2082
|
+
detail = (result.stdout + "\n" + result.stderr).strip()
|
|
2083
|
+
return failed(check, detail[-2000:] or f"pytest exited {result.returncode}")
|
|
2084
|
+
return passed(check)
|
|
2085
|
+
|
|
2086
|
+
|
|
2087
|
+
def check_cli_feature(
|
|
2088
|
+
workspace: Path,
|
|
2089
|
+
flag: str,
|
|
2090
|
+
input_text: str,
|
|
2091
|
+
expected_output: str,
|
|
2092
|
+
test_marker: str,
|
|
2093
|
+
) -> dict[str, str]:
|
|
2094
|
+
check = f"feature_{flag.lstrip('-').replace('-', '_')}"
|
|
2095
|
+
project_tests = check_pytest(workspace, check)
|
|
2096
|
+
if project_tests["status"] == "failed":
|
|
2097
|
+
return project_tests
|
|
2098
|
+
try:
|
|
2099
|
+
result = subprocess.run(
|
|
2100
|
+
[sys.executable, "wordcount.py", flag],
|
|
2101
|
+
cwd=workspace,
|
|
2102
|
+
input=input_text,
|
|
2103
|
+
capture_output=True,
|
|
2104
|
+
text=True,
|
|
2105
|
+
timeout=10,
|
|
2106
|
+
check=False,
|
|
2107
|
+
)
|
|
2108
|
+
except Exception as error:
|
|
2109
|
+
return failed(check, str(error))
|
|
2110
|
+
if result.returncode != 0 or expected_output not in result.stdout:
|
|
2111
|
+
return failed(
|
|
2112
|
+
check,
|
|
2113
|
+
f"Expected {expected_output!r}; exit={result.returncode}, stdout={result.stdout!r}",
|
|
2114
|
+
)
|
|
2115
|
+
tests = workspace / "test_wordcount.py"
|
|
2116
|
+
if not tests.is_file() or test_marker.lower() not in tests.read_text(encoding="utf-8").lower():
|
|
2117
|
+
return failed(check, f"No focused test marker {test_marker!r} was found")
|
|
2118
|
+
return passed(check)
|
|
2119
|
+
|
|
2120
|
+
|
|
2121
|
+
def check_native_isolation(workspace: Path) -> dict[str, str]:
|
|
2122
|
+
check = "native_isolation"
|
|
2123
|
+
comet_config_dir = workspace / ".comet"
|
|
2124
|
+
hidden_entries = (
|
|
2125
|
+
{path.name for path in comet_config_dir.iterdir()}
|
|
2126
|
+
if comet_config_dir.is_dir()
|
|
2127
|
+
else set()
|
|
2128
|
+
)
|
|
2129
|
+
present = []
|
|
2130
|
+
if (workspace / "openspec").exists():
|
|
2131
|
+
present.append("openspec")
|
|
2132
|
+
present.extend(f".comet/{name}" for name in sorted(hidden_entries - {"config.yaml"}))
|
|
2133
|
+
if present:
|
|
2134
|
+
return failed(check, f"Forbidden workflow artifacts exist: {present}")
|
|
2135
|
+
return passed(check)
|
|
2136
|
+
|
|
2137
|
+
|
|
2138
|
+
def write_results(
|
|
2139
|
+
results: list[dict[str, str]],
|
|
2140
|
+
workspace: Path,
|
|
2141
|
+
results_file: str | None = None,
|
|
2142
|
+
) -> int:
|
|
2143
|
+
output = {
|
|
2144
|
+
"passed": [result["check"] for result in results if result["status"] == "passed"],
|
|
2145
|
+
"failed": [
|
|
2146
|
+
f"{result['check']}: {result.get('reason', '')}"
|
|
2147
|
+
for result in results
|
|
2148
|
+
if result["status"] == "failed"
|
|
2149
|
+
],
|
|
2150
|
+
}
|
|
2151
|
+
target = results_file or os.environ.get("BENCH_TEST_RESULTS", "_test_results.json")
|
|
2152
|
+
(workspace / target).write_text(json.dumps(output, indent=2), encoding="utf-8")
|
|
2153
|
+
print(json.dumps(output))
|
|
2154
|
+
return 1 if output["failed"] else 0
|