supipowers 1.5.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -8
- package/bin/install.mjs +20 -5
- package/bin/install.ts +95 -0
- package/package.json +8 -4
- package/skills/context-mode/SKILL.md +17 -10
- package/skills/harness/SKILL.md +94 -0
- package/skills/ui-design/SKILL.md +63 -0
- package/skills/ui-design/sub-agent-templates/component-builder.md +29 -0
- package/skills/ui-design/sub-agent-templates/design-critic.md +46 -0
- package/skills/ui-design/sub-agent-templates/pencil/component-builder.md +29 -0
- package/skills/ui-design/sub-agent-templates/pencil/design-critic.md +42 -0
- package/skills/ui-design/sub-agent-templates/pencil/section-assembler.md +27 -0
- package/skills/ui-design/sub-agent-templates/section-assembler.md +27 -0
- package/skills/ultraplan-discover/SKILL.md +96 -0
- package/skills/ultraplan-intake/SKILL.md +89 -0
- package/skills/ultraplan-research/SKILL.md +129 -0
- package/skills/ultraplan-review/SKILL.md +86 -0
- package/skills/ultraplan-review-scope/SKILL.md +111 -0
- package/skills/ultraplan-review-structure/SKILL.md +120 -0
- package/skills/ultraplan-review-tdd/SKILL.md +142 -0
- package/skills/ultraplan-scout/SKILL.md +110 -0
- package/skills/ultraplan-synthesize/SKILL.md +124 -0
- package/src/{quality/ai-session.ts → ai/final-message.ts} +27 -0
- package/src/ai/schema-text.ts +129 -0
- package/src/ai/structured-output.ts +274 -0
- package/src/ai/template.ts +27 -0
- package/src/bootstrap.ts +63 -28
- package/src/commands/agents.ts +149 -45
- package/src/commands/ai-review.ts +251 -30
- package/src/commands/clear.ts +434 -0
- package/src/commands/commit.ts +1 -0
- package/src/commands/config.ts +242 -44
- package/src/commands/context.ts +55 -28
- package/src/commands/doctor.ts +234 -6
- package/src/commands/fix-pr.ts +306 -131
- package/src/commands/generate.ts +111 -21
- package/src/commands/memory.ts +192 -0
- package/src/commands/model-picker.ts +28 -21
- package/src/commands/model.ts +19 -9
- package/src/commands/optimize-context.ts +408 -29
- package/src/commands/plan.ts +2 -0
- package/src/commands/qa.ts +312 -137
- package/src/commands/release.ts +259 -76
- package/src/commands/review.ts +293 -59
- package/src/commands/status.ts +200 -13
- package/src/commands/supi.ts +3 -35
- package/src/commands/ui-design.ts +394 -0
- package/src/commands/ultraplan.ts +1518 -0
- package/src/commands/update.ts +86 -0
- package/src/config/defaults.ts +62 -0
- package/src/config/loader.ts +448 -60
- package/src/config/schema.ts +108 -2
- package/src/context/optimizer.ts +25 -33
- package/src/context/rule-renderer.ts +223 -0
- package/src/context/savings.ts +258 -0
- package/src/context/startup-check.ts +380 -0
- package/src/context/startup-optimizer.ts +355 -0
- package/src/context/tokenignore.ts +146 -0
- package/src/context-mode/cache-handle.ts +49 -0
- package/src/context-mode/cache-preview.ts +71 -0
- package/src/context-mode/cache-store.ts +738 -0
- package/src/context-mode/compressor.ts +131 -26
- package/src/context-mode/dedup.ts +108 -0
- package/src/context-mode/detector.ts +35 -4
- package/src/context-mode/event-extractor.ts +14 -12
- package/src/context-mode/event-store.ts +91 -36
- package/src/context-mode/hooks.ts +798 -56
- package/src/context-mode/knowledge/store.ts +255 -11
- package/src/context-mode/memory-store.ts +325 -0
- package/src/context-mode/metrics-recorder.ts +158 -0
- package/src/context-mode/metrics-store.ts +765 -0
- package/src/context-mode/model.ts +24 -0
- package/src/context-mode/processor-keys.ts +29 -0
- package/src/context-mode/processors/build.ts +66 -0
- package/src/context-mode/processors/docker.ts +57 -0
- package/src/context-mode/processors/git.ts +111 -0
- package/src/context-mode/processors/json.ts +112 -0
- package/src/context-mode/processors/k8s.ts +67 -0
- package/src/context-mode/processors/lint.ts +67 -0
- package/src/context-mode/processors/log.ts +86 -0
- package/src/context-mode/processors/registry.ts +116 -0
- package/src/context-mode/processors/test-runner.ts +102 -0
- package/src/context-mode/processors/types.ts +20 -0
- package/src/context-mode/repomap.ts +400 -0
- package/src/context-mode/routing.ts +97 -24
- package/src/context-mode/sandbox/runners.ts +5 -1
- package/src/context-mode/snapshot-builder.ts +106 -11
- package/src/context-mode/source-hash.ts +173 -0
- package/src/context-mode/tool-name.ts +11 -0
- package/src/context-mode/tools.ts +654 -22
- package/src/context-mode/web/fetcher.ts +31 -12
- package/src/debug/logger.ts +2 -1
- package/src/deps/registry.ts +1 -1
- package/src/discipline/failure-summarizer.ts +170 -0
- package/src/discipline/failure-taxonomy.ts +131 -0
- package/src/discipline/workflow-invariants.ts +125 -0
- package/src/discovery/index.ts +31 -0
- package/src/discovery/lsp.ts +87 -0
- package/src/discovery/rank.ts +144 -0
- package/src/discovery/sources.ts +89 -0
- package/src/discovery/workflow.ts +87 -0
- package/src/docs/contracts.ts +39 -0
- package/src/docs/drift.ts +117 -87
- package/src/fix-pr/assessment.ts +200 -0
- package/src/fix-pr/contracts.ts +47 -0
- package/src/fix-pr/fetch-comments.ts +80 -0
- package/src/fix-pr/prompt-builder.ts +58 -40
- package/src/fix-pr/scripts/exec.ts +34 -0
- package/src/fix-pr/scripts/trigger-review.ts +106 -0
- package/src/fix-pr/scripts/wait-and-check.ts +108 -0
- package/src/fix-pr/types.ts +4 -0
- package/src/git/branch-finish.ts +5 -0
- package/src/git/commit-contract.ts +83 -0
- package/src/git/commit.ts +121 -184
- package/src/git/status.ts +62 -8
- package/src/harness/anti_slop/architecture-parser.ts +210 -0
- package/src/harness/anti_slop/backend-factory.ts +30 -0
- package/src/harness/anti_slop/backend.ts +140 -0
- package/src/harness/anti_slop/desloppify-adapter.ts +319 -0
- package/src/harness/anti_slop/fallow-adapter.ts +305 -0
- package/src/harness/anti_slop/installer.ts +227 -0
- package/src/harness/anti_slop/queue.ts +216 -0
- package/src/harness/anti_slop/recommend.ts +84 -0
- package/src/harness/anti_slop/score.ts +180 -0
- package/src/harness/anti_slop/synthetic-edit-test.ts +128 -0
- package/src/harness/artifacts/agents-md.ts +88 -0
- package/src/harness/artifacts/checks-wiring.ts +57 -0
- package/src/harness/artifacts/docs-tree.ts +79 -0
- package/src/harness/artifacts/lint-configs.ts +136 -0
- package/src/harness/artifacts/review-agents.ts +67 -0
- package/src/harness/bare-entry.ts +108 -0
- package/src/harness/command.ts +1010 -0
- package/src/harness/default-agents/design.md +23 -0
- package/src/harness/default-agents/discover.md +18 -0
- package/src/harness/default-agents/implement.md +24 -0
- package/src/harness/default-agents/plan.md +19 -0
- package/src/harness/default-agents/research.md +21 -0
- package/src/harness/default-agents/validate.md +22 -0
- package/src/harness/gc/reporter.ts +28 -0
- package/src/harness/gc/runner.ts +136 -0
- package/src/harness/hooks/layer-context-inject.ts +155 -0
- package/src/harness/hooks/post-session-sweep.ts +130 -0
- package/src/harness/hooks/pre-edit-dupe-probe.ts +224 -0
- package/src/harness/hooks/register.ts +118 -0
- package/src/harness/model.ts +117 -0
- package/src/harness/pipeline.ts +348 -0
- package/src/harness/project-paths.ts +235 -0
- package/src/harness/stage-runner.ts +107 -0
- package/src/harness/stages/design.ts +386 -0
- package/src/harness/stages/discover.ts +454 -0
- package/src/harness/stages/implement.ts +162 -0
- package/src/harness/stages/plan.ts +335 -0
- package/src/harness/stages/research.ts +263 -0
- package/src/harness/stages/validate.ts +684 -0
- package/src/harness/storage.ts +467 -0
- package/src/harness/tools.ts +426 -0
- package/src/lsp/bridge.ts +56 -95
- package/src/lsp/capabilities.ts +108 -0
- package/src/lsp/contracts.ts +35 -0
- package/src/lsp/detector.ts +8 -12
- package/src/markdown-frontmatter.ts +68 -0
- package/src/mempalace/bridge.ts +129 -0
- package/src/mempalace/config.ts +75 -0
- package/src/mempalace/format.ts +163 -0
- package/src/mempalace/hooks.ts +370 -0
- package/src/mempalace/installer-helper.ts +194 -0
- package/src/mempalace/python/mempalace_bridge.py +440 -0
- package/src/mempalace/runtime.ts +565 -0
- package/src/mempalace/schema.ts +264 -0
- package/src/mempalace/session-summary.ts +198 -0
- package/src/mempalace/tool.ts +186 -0
- package/src/mempalace/uv.ts +256 -0
- package/src/migrate/runner.ts +354 -0
- package/src/planning/approval-flow.ts +206 -9
- package/src/planning/plan-writer-prompt.ts +4 -3
- package/src/planning/planning-ask-tool.ts +39 -0
- package/src/planning/render-markdown.ts +74 -0
- package/src/planning/spec.ts +42 -0
- package/src/planning/system-prompt.ts +11 -8
- package/src/planning/validate.ts +84 -0
- package/src/platform/omp.ts +15 -2
- package/src/platform/system-prompt.ts +37 -0
- package/src/platform/test-utils.ts +3 -0
- package/src/platform/types.ts +6 -1
- package/src/qa/config.ts +12 -6
- package/src/qa/detect-app-type.ts +13 -6
- package/src/qa/matrix.ts +12 -6
- package/src/qa/prompt-builder.ts +28 -30
- package/src/qa/scripts/dev-server-utils.ts +72 -0
- package/src/qa/scripts/run-e2e-tests.ts +226 -0
- package/src/qa/scripts/start-dev-server.ts +138 -0
- package/src/qa/scripts/stop-dev-server.ts +77 -0
- package/src/qa/session.ts +13 -7
- package/src/quality/ai-setup.ts +27 -25
- package/src/quality/contracts.ts +34 -0
- package/src/quality/gates/ai-review.ts +20 -58
- package/src/quality/gates/command.ts +249 -46
- package/src/quality/review-gates.ts +18 -2
- package/src/quality/runner.ts +63 -22
- package/src/quality/schemas.ts +37 -2
- package/src/quality/setup.ts +96 -16
- package/src/release/changelog.ts +1 -1
- package/src/release/channels/custom.ts +13 -3
- package/src/release/channels/types.ts +5 -0
- package/src/release/contracts.ts +90 -0
- package/src/release/executor.ts +122 -45
- package/src/release/prompt.ts +18 -2
- package/src/release/targets.ts +86 -0
- package/src/release/version.ts +96 -71
- package/src/review/agent-loader.ts +298 -127
- package/src/review/fixer.ts +10 -6
- package/src/review/multi-agent-runner.ts +115 -14
- package/src/review/output.ts +12 -139
- package/src/review/runner.ts +12 -6
- package/src/review/scope.ts +144 -24
- package/src/review/types.ts +11 -20
- package/src/review/validator.ts +12 -6
- package/src/storage/fix-pr-sessions.ts +21 -14
- package/src/storage/plans.ts +14 -5
- package/src/storage/qa-sessions.ts +25 -19
- package/src/storage/reliability-metrics.ts +180 -0
- package/src/storage/reports.ts +8 -7
- package/src/storage/review-sessions.ts +55 -20
- package/src/tool-catalog/active-tool-controller.ts +164 -0
- package/src/tool-catalog/active-tool-planner.ts +212 -0
- package/src/tool-catalog/tool-groups.ts +102 -0
- package/src/types.ts +1401 -5
- package/src/ui-design/backend-adapter.ts +78 -0
- package/src/ui-design/backends/local-html.ts +82 -0
- package/src/ui-design/backends/pencil-mcp.ts +111 -0
- package/src/ui-design/components-scanner.ts +124 -0
- package/src/ui-design/config.ts +55 -0
- package/src/ui-design/pen-scanner.ts +95 -0
- package/src/ui-design/pen-selector.ts +72 -0
- package/src/ui-design/prompt-builder.ts +73 -0
- package/src/ui-design/scanner.ts +136 -0
- package/src/ui-design/session.ts +974 -0
- package/src/ui-design/system-prompt.ts +312 -0
- package/src/ui-design/tokens-scanner.ts +181 -0
- package/src/ui-design/types.ts +96 -0
- package/src/ultraplan/agent-catalog.ts +522 -0
- package/src/ultraplan/authoring/agent-catalog.ts +310 -0
- package/src/ultraplan/authoring/authoring-tools.ts +552 -0
- package/src/ultraplan/authoring/command-handlers.ts +339 -0
- package/src/ultraplan/authoring/markdown.ts +510 -0
- package/src/ultraplan/authoring/model.ts +162 -0
- package/src/ultraplan/authoring/pipeline.ts +319 -0
- package/src/ultraplan/authoring/stage-runner.ts +141 -0
- package/src/ultraplan/authoring/stages/approve.ts +249 -0
- package/src/ultraplan/authoring/stages/discover.ts +289 -0
- package/src/ultraplan/authoring/stages/intake.ts +203 -0
- package/src/ultraplan/authoring/stages/research.ts +399 -0
- package/src/ultraplan/authoring/stages/review.ts +333 -0
- package/src/ultraplan/authoring/stages/scout.ts +188 -0
- package/src/ultraplan/authoring/stages/synthesize.ts +348 -0
- package/src/ultraplan/authoring/storage.ts +594 -0
- package/src/ultraplan/authoring/synth-gate.ts +165 -0
- package/src/ultraplan/authoring-draft.ts +653 -0
- package/src/ultraplan/authoring-persist.ts +180 -0
- package/src/ultraplan/authoring-tool.ts +608 -0
- package/src/ultraplan/authoring-wizard.ts +587 -0
- package/src/ultraplan/batch/merge.ts +98 -0
- package/src/ultraplan/batch/planner.ts +150 -0
- package/src/ultraplan/batch/presenter.ts +97 -0
- package/src/ultraplan/batch/storage.ts +420 -0
- package/src/ultraplan/batch/supervisor.ts +317 -0
- package/src/ultraplan/batch/worker.ts +26 -0
- package/src/ultraplan/batch/worktree.ts +110 -0
- package/src/ultraplan/contracts.ts +1593 -0
- package/src/ultraplan/default-agents/authoring/discoverer.md +12 -0
- package/src/ultraplan/default-agents/authoring/intake.md +12 -0
- package/src/ultraplan/default-agents/authoring/planner.md +12 -0
- package/src/ultraplan/default-agents/authoring/researcher.md +12 -0
- package/src/ultraplan/default-agents/authoring/scope-checker.md +12 -0
- package/src/ultraplan/default-agents/authoring/scout.md +12 -0
- package/src/ultraplan/default-agents/authoring/structure-checker.md +12 -0
- package/src/ultraplan/default-agents/authoring/tdd-checker.md +12 -0
- package/src/ultraplan/default-agents/backend-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/backend-executor.md +10 -0
- package/src/ultraplan/default-agents/backend-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/backend-tester.md +10 -0
- package/src/ultraplan/default-agents/frontend-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/frontend-executor.md +10 -0
- package/src/ultraplan/default-agents/frontend-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/frontend-tester.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-executor.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-tester.md +10 -0
- package/src/ultraplan/execution/contract.ts +71 -0
- package/src/ultraplan/execution/policy.ts +217 -0
- package/src/ultraplan/execution/runtime-tools.ts +107 -0
- package/src/ultraplan/execution/session-runner.ts +281 -0
- package/src/ultraplan/next-router.ts +85 -0
- package/src/ultraplan/presenter.ts +359 -0
- package/src/ultraplan/project-paths.ts +342 -0
- package/src/ultraplan/runtime/active-execution.ts +72 -0
- package/src/ultraplan/runtime/apply-mutation.ts +416 -0
- package/src/ultraplan/runtime/blockers.ts +243 -0
- package/src/ultraplan/runtime/hook-bridge.ts +486 -0
- package/src/ultraplan/runtime/launch-context.ts +207 -0
- package/src/ultraplan/runtime/migration.ts +524 -0
- package/src/ultraplan/runtime/normalize.ts +281 -0
- package/src/ultraplan/runtime/proof.ts +260 -0
- package/src/ultraplan/runtime/reducer.ts +416 -0
- package/src/ultraplan/runtime/repair.ts +251 -0
- package/src/ultraplan/runtime/tracker-storage.ts +368 -0
- package/src/ultraplan/session-selection.ts +291 -0
- package/src/ultraplan/storage.ts +374 -0
- package/src/utils/editor.ts +38 -0
- package/src/utils/executable.ts +80 -0
- package/src/utils/paths.ts +1 -20
- package/src/utils/shell.ts +31 -0
- package/src/visual/companion.ts +2 -1
- package/src/visual/scripts/frame-template.html +60 -0
- package/src/visual/scripts/index.js +59 -13
- package/src/visual/scripts/package.json +3 -0
- package/src/visual/start-server.ts +2 -1
- package/src/workspace/git-scope.ts +64 -0
- package/src/workspace/locks.ts +23 -0
- package/src/workspace/package-manager.ts +117 -0
- package/src/workspace/path-mapping.ts +75 -0
- package/src/workspace/project-slug.ts +92 -0
- package/src/workspace/repo-root.ts +137 -0
- package/src/workspace/selector.ts +115 -0
- package/src/workspace/state-paths.ts +118 -0
- package/src/workspace/targets.ts +313 -0
- package/src/fix-pr/scripts/diff-comments.sh +0 -33
- package/src/fix-pr/scripts/fetch-pr-comments.sh +0 -25
- package/src/fix-pr/scripts/trigger-review.sh +0 -36
- package/src/fix-pr/scripts/wait-and-check.sh +0 -37
- package/src/qa/scripts/detect-app-type.sh +0 -68
- package/src/qa/scripts/discover-routes.sh +0 -143
- package/src/qa/scripts/run-e2e-tests.sh +0 -131
- package/src/qa/scripts/start-dev-server.sh +0 -46
- package/src/qa/scripts/stop-dev-server.sh +0 -36
- package/src/review/prompts/fix-output-schema.md +0 -18
- package/src/review/prompts/review-output-schema.md +0 -38
- package/src/review/template.ts +0 -15
- /package/src/{review → ai}/prompts/invalid-output-retry.md +0 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
UltraPlanActorKind,
|
|
4
|
+
UltraPlanAttemptRecord,
|
|
5
|
+
UltraPlanCursorTargetType,
|
|
6
|
+
UltraPlanExecutionPhase,
|
|
7
|
+
UltraPlanHookEventName,
|
|
8
|
+
UltraPlanHookObservation,
|
|
9
|
+
UltraPlanObservationTarget,
|
|
10
|
+
UltraPlanScenarioLevel,
|
|
11
|
+
UltraPlanSourceAgent,
|
|
12
|
+
UltraPlanStackId,
|
|
13
|
+
} from "../../types.js";
|
|
14
|
+
import { recoverLaunchContextFromEvent, recoverTargetHintFromPrompt } from "./launch-context.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Slice-2 normalization seam.
|
|
18
|
+
*
|
|
19
|
+
* Converts raw platform hook events into typed `UltraPlanHookObservation` values. This module is
|
|
20
|
+
* the gate between raw platform behavior and UltraPlan runtime truth — by the time an observation
|
|
21
|
+
* reaches the reducer, its attempt identity, target, replay fingerprint, and correlation status
|
|
22
|
+
* are all fully resolved. Pure.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface UltraPlanTargetHint {
|
|
26
|
+
targetType?: UltraPlanCursorTargetType;
|
|
27
|
+
stack?: UltraPlanStackId | null;
|
|
28
|
+
domainId?: string | null;
|
|
29
|
+
level?: UltraPlanScenarioLevel | null;
|
|
30
|
+
scenarioId?: string | null;
|
|
31
|
+
phase?: UltraPlanExecutionPhase;
|
|
32
|
+
resolvedSlot?: string | null;
|
|
33
|
+
actorKind?: UltraPlanActorKind;
|
|
34
|
+
sourceAgent?: UltraPlanSourceAgent;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface NormalizeHookEventInput {
|
|
38
|
+
hookEvent: UltraPlanHookEventName;
|
|
39
|
+
sessionId: string;
|
|
40
|
+
nowIso: string;
|
|
41
|
+
|
|
42
|
+
/** Structured platform metadata (may carry `ultraplanLaunchContext`). */
|
|
43
|
+
metadata?: Record<string, unknown> | null;
|
|
44
|
+
/** Prompt/assignment/system-prompt text (may carry `ULTRAPLAN_LAUNCH_CONTEXT=<json>`). */
|
|
45
|
+
prompt?: string | null;
|
|
46
|
+
/** The currently-persisted active attempt from the tracker (last-resort carrier). */
|
|
47
|
+
persistedActiveAttempt?: UltraPlanAttemptRecord | null;
|
|
48
|
+
|
|
49
|
+
/** Raw platform payload (tool args, results, exit reasons, etc.) — used for audit + fingerprint. */
|
|
50
|
+
payload?: Record<string, unknown>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Hints about the slot-backed target derived from the platform event (e.g. before_agent_start
|
|
54
|
+
* args). When absent or the hook is a pure session event, the observation is classified as
|
|
55
|
+
* session-scope.
|
|
56
|
+
*/
|
|
57
|
+
targetHint?: UltraPlanTargetHint;
|
|
58
|
+
/** Fallback hint from active-execution state when prompt text did not carry one. */
|
|
59
|
+
fallbackTargetHint?: UltraPlanTargetHint;
|
|
60
|
+
|
|
61
|
+
/** Optional platform-native event id (tool call id, turn id, agent run id). */
|
|
62
|
+
nativeEventId?: string | null;
|
|
63
|
+
/** Optional causation id grouping related hook activity. */
|
|
64
|
+
causationId?: string | null;
|
|
65
|
+
/** Optional override for when the event actually occurred; defaults to `nowIso`. */
|
|
66
|
+
occurredAt?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Hook events that are session-scope regardless of target hint. Main-orchestrator-only
|
|
71
|
+
* before_agent_start / tool_call / tool_result / agent_end events that arrive with no slot-backed
|
|
72
|
+
* target hint are also treated as session-scope via the classification logic below.
|
|
73
|
+
*/
|
|
74
|
+
const SESSION_SCOPE_HOOKS: readonly UltraPlanHookEventName[] = [
|
|
75
|
+
"session_start",
|
|
76
|
+
"session_shutdown",
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
export function normalizeHookEvent(input: NormalizeHookEventInput): UltraPlanHookObservation {
|
|
80
|
+
const occurredAt = input.occurredAt ?? input.nowIso;
|
|
81
|
+
const targetHint = resolveTargetHint(input);
|
|
82
|
+
const launchContext = SESSION_SCOPE_HOOKS.includes(input.hookEvent)
|
|
83
|
+
? null
|
|
84
|
+
: recoverLaunchContextFromEvent({
|
|
85
|
+
metadata: input.metadata ?? null,
|
|
86
|
+
prompt: input.prompt ?? null,
|
|
87
|
+
persistedActiveAttempt: input.persistedActiveAttempt ?? null,
|
|
88
|
+
});
|
|
89
|
+
const { actorKind, isSessionScope } = classifyActor(input, targetHint, launchContext);
|
|
90
|
+
const sourceAgent = targetHint?.sourceAgent
|
|
91
|
+
?? launchContext?.sourceAgent
|
|
92
|
+
?? (isSessionScope ? "main" : "sub-agent");
|
|
93
|
+
|
|
94
|
+
if (isSessionScope) {
|
|
95
|
+
return {
|
|
96
|
+
sessionId: input.sessionId,
|
|
97
|
+
hookEvent: input.hookEvent,
|
|
98
|
+
actorKind: "main-orchestrator",
|
|
99
|
+
attemptId: null,
|
|
100
|
+
attemptKey: null,
|
|
101
|
+
sourceAgent,
|
|
102
|
+
occurredAt,
|
|
103
|
+
causationId: input.causationId ?? null,
|
|
104
|
+
fingerprint: computeFingerprint({
|
|
105
|
+
attemptId: null,
|
|
106
|
+
hookEvent: input.hookEvent,
|
|
107
|
+
nativeEventId: input.nativeEventId ?? null,
|
|
108
|
+
payload: input.payload ?? {},
|
|
109
|
+
}),
|
|
110
|
+
target: null,
|
|
111
|
+
correlationFailure: null,
|
|
112
|
+
payloadSummary: summarizePayload(input.hookEvent, input.payload),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!launchContext) {
|
|
117
|
+
return {
|
|
118
|
+
sessionId: input.sessionId,
|
|
119
|
+
hookEvent: input.hookEvent,
|
|
120
|
+
actorKind,
|
|
121
|
+
attemptId: null,
|
|
122
|
+
attemptKey: null,
|
|
123
|
+
sourceAgent,
|
|
124
|
+
occurredAt,
|
|
125
|
+
causationId: input.causationId ?? null,
|
|
126
|
+
fingerprint: computeFingerprint({
|
|
127
|
+
attemptId: null,
|
|
128
|
+
hookEvent: input.hookEvent,
|
|
129
|
+
nativeEventId: input.nativeEventId ?? null,
|
|
130
|
+
payload: input.payload ?? {},
|
|
131
|
+
}),
|
|
132
|
+
target: buildTargetFromHint(targetHint),
|
|
133
|
+
correlationFailure: {
|
|
134
|
+
reason: "slot-backed hook event without a resolvable UltraPlan launch context",
|
|
135
|
+
},
|
|
136
|
+
payloadSummary: summarizePayload(input.hookEvent, input.payload),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!targetHint) {
|
|
141
|
+
return {
|
|
142
|
+
sessionId: input.sessionId,
|
|
143
|
+
hookEvent: input.hookEvent,
|
|
144
|
+
actorKind,
|
|
145
|
+
attemptId: launchContext.attemptId,
|
|
146
|
+
attemptKey: launchContext.attemptKey,
|
|
147
|
+
sourceAgent,
|
|
148
|
+
occurredAt,
|
|
149
|
+
causationId: input.causationId ?? null,
|
|
150
|
+
fingerprint: computeFingerprint({
|
|
151
|
+
attemptId: launchContext.attemptId,
|
|
152
|
+
hookEvent: input.hookEvent,
|
|
153
|
+
nativeEventId: input.nativeEventId ?? null,
|
|
154
|
+
payload: input.payload ?? {},
|
|
155
|
+
}),
|
|
156
|
+
target: null,
|
|
157
|
+
correlationFailure: {
|
|
158
|
+
reason: "slot-backed hook event without a resolvable UltraPlan target hint",
|
|
159
|
+
},
|
|
160
|
+
payloadSummary: summarizePayload(input.hookEvent, input.payload),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
sessionId: input.sessionId,
|
|
166
|
+
hookEvent: input.hookEvent,
|
|
167
|
+
actorKind,
|
|
168
|
+
attemptId: launchContext.attemptId,
|
|
169
|
+
attemptKey: launchContext.attemptKey,
|
|
170
|
+
sourceAgent,
|
|
171
|
+
occurredAt,
|
|
172
|
+
causationId: input.causationId ?? null,
|
|
173
|
+
fingerprint: computeFingerprint({
|
|
174
|
+
attemptId: launchContext.attemptId,
|
|
175
|
+
hookEvent: input.hookEvent,
|
|
176
|
+
nativeEventId: input.nativeEventId ?? null,
|
|
177
|
+
payload: input.payload ?? {},
|
|
178
|
+
}),
|
|
179
|
+
target: buildTargetFromHint(targetHint),
|
|
180
|
+
correlationFailure: null,
|
|
181
|
+
payloadSummary: summarizePayload(input.hookEvent, input.payload),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- helpers ---------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
function classifyActor(
|
|
188
|
+
input: NormalizeHookEventInput,
|
|
189
|
+
targetHint: UltraPlanTargetHint | undefined,
|
|
190
|
+
launchContext: UltraPlanAttemptRecord["launchContext"] | null,
|
|
191
|
+
): { actorKind: UltraPlanActorKind; isSessionScope: boolean } {
|
|
192
|
+
if (SESSION_SCOPE_HOOKS.includes(input.hookEvent)) {
|
|
193
|
+
return { actorKind: "main-orchestrator", isSessionScope: true };
|
|
194
|
+
}
|
|
195
|
+
if (targetHint?.actorKind === "main-orchestrator") {
|
|
196
|
+
return { actorKind: "main-orchestrator", isSessionScope: true };
|
|
197
|
+
}
|
|
198
|
+
if (targetHint || launchContext) {
|
|
199
|
+
return { actorKind: targetHint?.actorKind ?? "slot", isSessionScope: false };
|
|
200
|
+
}
|
|
201
|
+
return { actorKind: "main-orchestrator", isSessionScope: true };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function buildTargetFromHint(hint: UltraPlanTargetHint | undefined): UltraPlanObservationTarget | null {
|
|
205
|
+
if (!hint) return null;
|
|
206
|
+
const hasAnyField = hint.targetType !== undefined
|
|
207
|
+
|| hint.stack !== undefined
|
|
208
|
+
|| hint.domainId !== undefined
|
|
209
|
+
|| hint.level !== undefined
|
|
210
|
+
|| hint.scenarioId !== undefined
|
|
211
|
+
|| hint.phase !== undefined
|
|
212
|
+
|| hint.resolvedSlot !== undefined;
|
|
213
|
+
if (!hasAnyField) return null;
|
|
214
|
+
return {
|
|
215
|
+
targetType: hint.targetType ?? "scenario",
|
|
216
|
+
stack: hint.stack ?? null,
|
|
217
|
+
domainId: hint.domainId ?? null,
|
|
218
|
+
level: hint.level ?? null,
|
|
219
|
+
scenarioId: hint.scenarioId ?? null,
|
|
220
|
+
phase: hint.phase ?? "red",
|
|
221
|
+
resolvedSlot: hint.resolvedSlot ?? null,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function resolveTargetHint(input: NormalizeHookEventInput): UltraPlanTargetHint | undefined {
|
|
226
|
+
return input.targetHint
|
|
227
|
+
?? recoverTargetHintFromPrompt(input.prompt ?? null)
|
|
228
|
+
?? input.fallbackTargetHint;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
interface FingerprintComponents {
|
|
232
|
+
attemptId: string | null;
|
|
233
|
+
hookEvent: UltraPlanHookEventName;
|
|
234
|
+
nativeEventId: string | null;
|
|
235
|
+
payload: unknown;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Replay fingerprint per spec §cross-hook carrier line 459:
|
|
240
|
+
* `attemptId + hook name + native event id + normalized payload`
|
|
241
|
+
*
|
|
242
|
+
* The payload is canonicalized (sorted keys) so equivalent payloads with different key ordering
|
|
243
|
+
* produce the same fingerprint. This is what makes replay dedupe durable across reloads.
|
|
244
|
+
*/
|
|
245
|
+
function computeFingerprint(parts: FingerprintComponents): string {
|
|
246
|
+
const canonical = JSON.stringify({
|
|
247
|
+
attemptId: parts.attemptId,
|
|
248
|
+
hookEvent: parts.hookEvent,
|
|
249
|
+
nativeEventId: parts.nativeEventId,
|
|
250
|
+
payload: canonicalize(parts.payload),
|
|
251
|
+
});
|
|
252
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function canonicalize(value: unknown): unknown {
|
|
256
|
+
if (Array.isArray(value)) {
|
|
257
|
+
return value.map((item) => canonicalize(item));
|
|
258
|
+
}
|
|
259
|
+
if (value && typeof value === "object") {
|
|
260
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
261
|
+
.filter(([, v]) => v !== undefined)
|
|
262
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
263
|
+
.map(([k, v]) => [k, canonicalize(v)] as const);
|
|
264
|
+
return Object.fromEntries(entries);
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function summarizePayload(hookEvent: UltraPlanHookEventName, payload: Record<string, unknown> | undefined): string {
|
|
270
|
+
if (!payload) return hookEvent;
|
|
271
|
+
const tool = typeof payload.toolName === "string" ? payload.toolName : undefined;
|
|
272
|
+
const exit = typeof payload.exitCode === "number" ? payload.exitCode : undefined;
|
|
273
|
+
const reason = typeof payload.exitReason === "string" ? payload.exitReason : undefined;
|
|
274
|
+
const summary = typeof payload.summary === "string" ? payload.summary : undefined;
|
|
275
|
+
const bits: string[] = [hookEvent];
|
|
276
|
+
if (tool) bits.push(`tool=${tool}`);
|
|
277
|
+
if (exit !== undefined) bits.push(`exit=${exit}`);
|
|
278
|
+
if (reason) bits.push(`reason=${reason}`);
|
|
279
|
+
if (summary) bits.push(summary);
|
|
280
|
+
return bits.join(" ");
|
|
281
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
UltraPlanBlockerCandidate,
|
|
4
|
+
UltraPlanExecutionPhase,
|
|
5
|
+
UltraPlanHookObservation,
|
|
6
|
+
UltraPlanProofCandidate,
|
|
7
|
+
UltraPlanProofCandidateTarget,
|
|
8
|
+
UltraPlanStackId,
|
|
9
|
+
} from "../../types.js";
|
|
10
|
+
import {
|
|
11
|
+
isUltraPlanDomainReview,
|
|
12
|
+
isUltraPlanStackReview,
|
|
13
|
+
} from "../contracts.js";
|
|
14
|
+
import {
|
|
15
|
+
buildProofInvalidBlocker,
|
|
16
|
+
} from "./blockers.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Slice-2 proof extraction.
|
|
20
|
+
*
|
|
21
|
+
* Converts `tool_result` / `agent_end` observations into typed proof candidates. Fail-closed:
|
|
22
|
+
* phase or target mismatches become `proof-invalid` blocker candidates, never silent advancement.
|
|
23
|
+
* Pure — no I/O.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export interface ExtractProofInput {
|
|
27
|
+
observation: UltraPlanHookObservation;
|
|
28
|
+
/**
|
|
29
|
+
* Raw platform payload. When the payload has a structured `proof` object of shape
|
|
30
|
+
* `{ type, phase, evidence, artifactRef? }`, that value is treated as a proof candidate.
|
|
31
|
+
*/
|
|
32
|
+
payload: Record<string, unknown>;
|
|
33
|
+
expectedTarget: UltraPlanProofCandidateTarget;
|
|
34
|
+
expectedPhase: UltraPlanExecutionPhase;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type ExtractProofResult =
|
|
38
|
+
| { kind: "proof"; proof: UltraPlanProofCandidate }
|
|
39
|
+
| { kind: "blocker-candidate"; blocker: UltraPlanBlockerCandidate }
|
|
40
|
+
| { kind: "none" };
|
|
41
|
+
|
|
42
|
+
export function extractProofCandidate(input: ExtractProofInput): ExtractProofResult {
|
|
43
|
+
const raw = input.payload?.proof;
|
|
44
|
+
if (raw === undefined || raw === null) {
|
|
45
|
+
return { kind: "none" };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const parsed = parseProofShape(raw);
|
|
49
|
+
if (!parsed) {
|
|
50
|
+
return {
|
|
51
|
+
kind: "blocker-candidate",
|
|
52
|
+
blocker: {
|
|
53
|
+
blocker: buildProofInvalidBlocker({
|
|
54
|
+
detectedAt: input.observation.occurredAt,
|
|
55
|
+
scope: "scenario",
|
|
56
|
+
affected: toAffected(input.expectedTarget),
|
|
57
|
+
reason: "payload.proof is not a well-formed proof object",
|
|
58
|
+
}),
|
|
59
|
+
observationFingerprint: input.observation.fingerprint,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Phase mismatch: reducer spec §proof obligations requires exact phase alignment.
|
|
65
|
+
if (parsed.phase !== input.expectedPhase) {
|
|
66
|
+
return {
|
|
67
|
+
kind: "blocker-candidate",
|
|
68
|
+
blocker: {
|
|
69
|
+
blocker: buildProofInvalidBlocker({
|
|
70
|
+
detectedAt: input.observation.occurredAt,
|
|
71
|
+
scope: "scenario",
|
|
72
|
+
affected: toAffected(input.expectedTarget),
|
|
73
|
+
reason: `expected ${input.expectedPhase}-phase proof, received ${parsed.phase}-phase proof`,
|
|
74
|
+
}),
|
|
75
|
+
observationFingerprint: input.observation.fingerprint,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Target mismatch: observation.target must align with the expected target.
|
|
81
|
+
if (!observationTargetMatchesExpected(input.observation, input.expectedTarget)) {
|
|
82
|
+
return {
|
|
83
|
+
kind: "blocker-candidate",
|
|
84
|
+
blocker: {
|
|
85
|
+
blocker: buildProofInvalidBlocker({
|
|
86
|
+
detectedAt: input.observation.occurredAt,
|
|
87
|
+
scope: "scenario",
|
|
88
|
+
affected: toAffected(input.expectedTarget),
|
|
89
|
+
reason: "proof target does not match the expected attempt target",
|
|
90
|
+
}),
|
|
91
|
+
observationFingerprint: input.observation.fingerprint,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const candidate: UltraPlanProofCandidate = {
|
|
97
|
+
phase: parsed.phase,
|
|
98
|
+
type: parsed.type,
|
|
99
|
+
target: input.expectedTarget,
|
|
100
|
+
evidence: parsed.evidence,
|
|
101
|
+
artifactRef: parsed.artifactRef ?? null,
|
|
102
|
+
observationFingerprint: input.observation.fingerprint,
|
|
103
|
+
fingerprint: computeProofFingerprint({
|
|
104
|
+
observationFingerprint: input.observation.fingerprint,
|
|
105
|
+
phase: parsed.phase,
|
|
106
|
+
type: parsed.type,
|
|
107
|
+
target: input.expectedTarget,
|
|
108
|
+
evidence: parsed.evidence,
|
|
109
|
+
artifactRef: parsed.artifactRef ?? null,
|
|
110
|
+
}),
|
|
111
|
+
};
|
|
112
|
+
return { kind: "proof", proof: candidate };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Review artifact validation (spec §proof obligations line 602)
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
export interface ValidateReviewArtifactInput {
|
|
120
|
+
reviewType: "domain" | "stack";
|
|
121
|
+
stack: UltraPlanStackId;
|
|
122
|
+
domainId: string | null;
|
|
123
|
+
expectedCanonicalPath: string;
|
|
124
|
+
observedArtifactRef: string;
|
|
125
|
+
artifact: unknown;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export type ValidateReviewArtifactResult =
|
|
129
|
+
| { ok: true }
|
|
130
|
+
| { ok: false; reason: string };
|
|
131
|
+
|
|
132
|
+
export function validateReviewArtifactProof(input: ValidateReviewArtifactInput): ValidateReviewArtifactResult {
|
|
133
|
+
if (input.observedArtifactRef !== input.expectedCanonicalPath) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
reason: `review artifact must live at canonical path ${input.expectedCanonicalPath}, observed ${input.observedArtifactRef}`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (input.reviewType === "domain") {
|
|
141
|
+
if (!isUltraPlanDomainReview(input.artifact)) {
|
|
142
|
+
return { ok: false, reason: "domain review artifact failed schema validation" };
|
|
143
|
+
}
|
|
144
|
+
const artifact = input.artifact;
|
|
145
|
+
if (artifact.status !== "passed") {
|
|
146
|
+
return { ok: false, reason: `domain review artifact status is ${artifact.status}, expected passed` };
|
|
147
|
+
}
|
|
148
|
+
if (artifact.stack !== input.stack || artifact.domainId !== input.domainId) {
|
|
149
|
+
return { ok: false, reason: "domain review artifact stack/domainId does not match review target" };
|
|
150
|
+
}
|
|
151
|
+
return { ok: true };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// stack review
|
|
155
|
+
if (!isUltraPlanStackReview(input.artifact)) {
|
|
156
|
+
return { ok: false, reason: "stack review artifact failed schema validation" };
|
|
157
|
+
}
|
|
158
|
+
const artifact = input.artifact;
|
|
159
|
+
if (artifact.status !== "passed") {
|
|
160
|
+
return { ok: false, reason: `stack review artifact status is ${artifact.status}, expected passed` };
|
|
161
|
+
}
|
|
162
|
+
if (artifact.stack !== input.stack) {
|
|
163
|
+
return { ok: false, reason: "stack review artifact stack does not match review target" };
|
|
164
|
+
}
|
|
165
|
+
return { ok: true };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// helpers
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
interface ParsedProofShape {
|
|
173
|
+
type: UltraPlanProofCandidate["type"];
|
|
174
|
+
phase: UltraPlanExecutionPhase;
|
|
175
|
+
evidence: { summary: string; command?: string; outputRef?: string; metadata?: Record<string, unknown> };
|
|
176
|
+
artifactRef?: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const VALID_PROOF_TYPES: readonly string[] = ["test", "command", "review", "artifact"];
|
|
180
|
+
const VALID_PHASES: readonly string[] = ["red", "green", "review", "waiting", "complete"];
|
|
181
|
+
|
|
182
|
+
function parseProofShape(raw: unknown): ParsedProofShape | null {
|
|
183
|
+
if (!raw || typeof raw !== "object") return null;
|
|
184
|
+
const obj = raw as Record<string, unknown>;
|
|
185
|
+
if (typeof obj.type !== "string" || !VALID_PROOF_TYPES.includes(obj.type)) return null;
|
|
186
|
+
if (typeof obj.phase !== "string" || !VALID_PHASES.includes(obj.phase)) return null;
|
|
187
|
+
if (!obj.evidence || typeof obj.evidence !== "object") return null;
|
|
188
|
+
const ev = obj.evidence as Record<string, unknown>;
|
|
189
|
+
if (typeof ev.summary !== "string" || ev.summary.length === 0) return null;
|
|
190
|
+
|
|
191
|
+
const parsed: ParsedProofShape = {
|
|
192
|
+
type: obj.type as UltraPlanProofCandidate["type"],
|
|
193
|
+
phase: obj.phase as UltraPlanExecutionPhase,
|
|
194
|
+
evidence: {
|
|
195
|
+
summary: ev.summary,
|
|
196
|
+
...(typeof ev.command === "string" ? { command: ev.command } : {}),
|
|
197
|
+
...(typeof ev.outputRef === "string" ? { outputRef: ev.outputRef } : {}),
|
|
198
|
+
...(ev.metadata && typeof ev.metadata === "object" && !Array.isArray(ev.metadata)
|
|
199
|
+
? { metadata: ev.metadata as Record<string, unknown> }
|
|
200
|
+
: {}),
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
if (typeof obj.artifactRef === "string" && obj.artifactRef.length > 0) {
|
|
204
|
+
parsed.artifactRef = obj.artifactRef;
|
|
205
|
+
}
|
|
206
|
+
return parsed;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function toAffected(target: UltraPlanProofCandidateTarget) {
|
|
210
|
+
return {
|
|
211
|
+
stack: target.stack,
|
|
212
|
+
domainId: target.domainId,
|
|
213
|
+
level: target.level,
|
|
214
|
+
scenarioId: target.scenarioId,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function observationTargetMatchesExpected(
|
|
219
|
+
observation: UltraPlanHookObservation,
|
|
220
|
+
expected: UltraPlanProofCandidateTarget,
|
|
221
|
+
): boolean {
|
|
222
|
+
const obsTarget = observation.target;
|
|
223
|
+
if (!obsTarget) return false;
|
|
224
|
+
return obsTarget.targetType === expected.targetType
|
|
225
|
+
&& obsTarget.stack === expected.stack
|
|
226
|
+
&& obsTarget.domainId === expected.domainId
|
|
227
|
+
&& obsTarget.level === expected.level
|
|
228
|
+
&& obsTarget.scenarioId === expected.scenarioId;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function computeProofFingerprint(parts: {
|
|
232
|
+
observationFingerprint: string;
|
|
233
|
+
phase: UltraPlanExecutionPhase;
|
|
234
|
+
type: UltraPlanProofCandidate["type"];
|
|
235
|
+
target: UltraPlanProofCandidateTarget;
|
|
236
|
+
evidence: ParsedProofShape["evidence"];
|
|
237
|
+
artifactRef: string | null;
|
|
238
|
+
}): string {
|
|
239
|
+
const canonical = JSON.stringify({
|
|
240
|
+
observationFingerprint: parts.observationFingerprint,
|
|
241
|
+
phase: parts.phase,
|
|
242
|
+
type: parts.type,
|
|
243
|
+
target: canonicalize(parts.target),
|
|
244
|
+
evidence: canonicalize(parts.evidence),
|
|
245
|
+
artifactRef: parts.artifactRef,
|
|
246
|
+
});
|
|
247
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function canonicalize(value: unknown): unknown {
|
|
251
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
252
|
+
if (value && typeof value === "object") {
|
|
253
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
254
|
+
.filter(([, v]) => v !== undefined)
|
|
255
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
256
|
+
.map(([k, v]) => [k, canonicalize(v)] as const);
|
|
257
|
+
return Object.fromEntries(entries);
|
|
258
|
+
}
|
|
259
|
+
return value;
|
|
260
|
+
}
|