supipowers 1.5.3 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +131 -42
- 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 +18 -8
- 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 +135 -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 +268 -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 +221 -109
- package/src/review/fixer.ts +10 -6
- package/src/review/multi-agent-runner.ts +114 -13
- 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 +1 -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 +1399 -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,524 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { PlatformPaths } from "../../platform/types.js";
|
|
5
|
+
import type {
|
|
6
|
+
UltraPlanAuthoredArtifact,
|
|
7
|
+
UltraPlanBlocker,
|
|
8
|
+
UltraPlanManifest,
|
|
9
|
+
UltraPlanSessionMigrationRecord,
|
|
10
|
+
} from "../../types.js";
|
|
11
|
+
import {
|
|
12
|
+
validateUltraPlanAuthoredArtifact,
|
|
13
|
+
validateUltraPlanManifest,
|
|
14
|
+
validateUltraPlanSessionMigrationRecord,
|
|
15
|
+
} from "../contracts.js";
|
|
16
|
+
import {
|
|
17
|
+
getLegacyUltraplanSessionDir,
|
|
18
|
+
getUltraplanAuthoredJsonPath,
|
|
19
|
+
getUltraplanManifestPath,
|
|
20
|
+
getUltraplanMigrationRecordPath,
|
|
21
|
+
getUltraplanSessionDir,
|
|
22
|
+
ULTRAPLAN_AUTHORED_JSON_FILENAME,
|
|
23
|
+
ULTRAPLAN_MANIFEST_FILENAME,
|
|
24
|
+
} from "../project-paths.js";
|
|
25
|
+
import {
|
|
26
|
+
buildMigrationConflictBlocker,
|
|
27
|
+
buildMigrationUnsafeBlocker,
|
|
28
|
+
} from "./blockers.js";
|
|
29
|
+
import { saveMigrationRecord } from "./tracker-storage.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Slice-2 migration engine.
|
|
33
|
+
*
|
|
34
|
+
* Implements the 7-branch decision procedure from the delta spec §per-session decision procedure.
|
|
35
|
+
* The engine runs fail-closed: any branch it cannot complete deterministically surfaces a
|
|
36
|
+
* structured `migration-unsafe` or `migration-conflict` blocker instead of a partially-migrated
|
|
37
|
+
* global directory.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
export interface ResolveSessionMigrationInput {
|
|
41
|
+
paths: PlatformPaths;
|
|
42
|
+
cwd: string;
|
|
43
|
+
sessionId: string;
|
|
44
|
+
nowIso: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type MigrationOutcome =
|
|
48
|
+
| { kind: "skip" }
|
|
49
|
+
| { kind: "native" }
|
|
50
|
+
| { kind: "migrated-copied"; record: UltraPlanSessionMigrationRecord }
|
|
51
|
+
| { kind: "reconciled-no-op"; record: UltraPlanSessionMigrationRecord }
|
|
52
|
+
| { kind: "blocked"; blocker: UltraPlanBlocker };
|
|
53
|
+
|
|
54
|
+
interface GlobalState {
|
|
55
|
+
exists: boolean;
|
|
56
|
+
authored: UltraPlanAuthoredArtifact | null;
|
|
57
|
+
manifest: UltraPlanManifest | null;
|
|
58
|
+
hasMigrationJson: boolean;
|
|
59
|
+
migrationJsonValid: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface LegacyState {
|
|
63
|
+
exists: boolean;
|
|
64
|
+
authored: UltraPlanAuthoredArtifact | null;
|
|
65
|
+
manifest: UltraPlanManifest | null;
|
|
66
|
+
/** True when authored.json and manifest.json are both present and pass schema validation. */
|
|
67
|
+
canonical: boolean;
|
|
68
|
+
legacyDir: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function resolveSessionMigration(input: ResolveSessionMigrationInput): MigrationOutcome {
|
|
72
|
+
const global = inspectGlobal(input);
|
|
73
|
+
const legacy = inspectLegacy(input);
|
|
74
|
+
|
|
75
|
+
// Branch 1: no global, no legacy.
|
|
76
|
+
if (!global.exists && !legacy.exists) {
|
|
77
|
+
return { kind: "skip" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Branch 6: global absent, legacy present.
|
|
81
|
+
if (!global.exists && legacy.exists) {
|
|
82
|
+
return migrateFromLegacy(input, legacy);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Global exists. Branch 3/4 only apply when the global directory is canonical per the delta
|
|
86
|
+
// spec definition: authored+manifest valid AND (no legacy OR migration.json present+valid).
|
|
87
|
+
// A global directory with matching content but no migration.json is partial/interrupted —
|
|
88
|
+
// the spec explicitly forbids the loader from accepting it as canonical on retry.
|
|
89
|
+
const globalHasValidArtifacts = global.authored !== null && global.manifest !== null;
|
|
90
|
+
const globalIsCanonical = globalHasValidArtifacts
|
|
91
|
+
&& (!legacy.exists || (global.hasMigrationJson && global.migrationJsonValid));
|
|
92
|
+
|
|
93
|
+
if (globalIsCanonical && legacy.exists && legacy.canonical) {
|
|
94
|
+
const fingerprintGlobal = fingerprintArtifacts(global.authored!, global.manifest!);
|
|
95
|
+
const fingerprintLegacy = fingerprintArtifacts(legacy.authored!, legacy.manifest!);
|
|
96
|
+
const contentsMatch =
|
|
97
|
+
fingerprintGlobal === fingerprintLegacy
|
|
98
|
+
&& global.manifest!.updatedAt === legacy.manifest!.updatedAt
|
|
99
|
+
&& sameCursor(global.manifest!.cursor, legacy.manifest!.cursor);
|
|
100
|
+
if (contentsMatch) {
|
|
101
|
+
return reconcileSameContent(input, legacy, fingerprintGlobal);
|
|
102
|
+
}
|
|
103
|
+
// Branch 4: both canonical-shaped but contents conflict.
|
|
104
|
+
return {
|
|
105
|
+
kind: "blocked",
|
|
106
|
+
blocker: buildMigrationConflictBlocker({
|
|
107
|
+
detectedAt: input.nowIso,
|
|
108
|
+
legacyPath: legacy.legacyDir,
|
|
109
|
+
globalPath: getUltraplanSessionDir(input.paths, input.cwd, input.sessionId),
|
|
110
|
+
reason: describeContentMismatch(global, legacy, fingerprintGlobal, fingerprintLegacy),
|
|
111
|
+
}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Branch 2: canonical global, no legacy.
|
|
116
|
+
if (globalIsCanonical && !legacy.exists) {
|
|
117
|
+
return { kind: "native" };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Branch 5: non-canonical global with a valid legacy copy. Rename the partial global directory,
|
|
121
|
+
// then migrate in from legacy via branch 6.
|
|
122
|
+
if (legacy.exists && legacy.canonical) {
|
|
123
|
+
return recoverFromPartialGlobal(input, legacy);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Branch 7: non-canonical global, no legacy. Rename the partial global directory and emit a
|
|
127
|
+
// migration-unsafe blocker naming the interrupted path.
|
|
128
|
+
return classifyOrphanedGlobal(input);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Branch 6 — copy legacy into global, then rename legacy
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
function migrateFromLegacy(input: ResolveSessionMigrationInput, legacy: LegacyState): MigrationOutcome {
|
|
136
|
+
if (!legacy.canonical || !legacy.authored || !legacy.manifest) {
|
|
137
|
+
return {
|
|
138
|
+
kind: "blocked",
|
|
139
|
+
blocker: buildMigrationUnsafeBlocker({
|
|
140
|
+
detectedAt: input.nowIso,
|
|
141
|
+
legacyPath: legacy.legacyDir,
|
|
142
|
+
reason: "legacy authored.json or manifest.json failed schema validation",
|
|
143
|
+
}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const globalDir = getUltraplanSessionDir(input.paths, input.cwd, input.sessionId);
|
|
148
|
+
|
|
149
|
+
// Durability order: copy tree, then write migration.json, then rename legacy.
|
|
150
|
+
try {
|
|
151
|
+
fs.mkdirSync(globalDir, { recursive: true });
|
|
152
|
+
copyDirectoryTree(legacy.legacyDir, globalDir);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return {
|
|
155
|
+
kind: "blocked",
|
|
156
|
+
blocker: buildMigrationUnsafeBlocker({
|
|
157
|
+
detectedAt: input.nowIso,
|
|
158
|
+
legacyPath: legacy.legacyDir,
|
|
159
|
+
reason: `copy failed before migration.json could be written: ${formatError(error)}`,
|
|
160
|
+
}),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const fingerprintBefore = fingerprintLegacyArtifacts(legacy.authored, legacy.manifest);
|
|
165
|
+
const fingerprintAfter = fingerprintGlobalArtifacts(input);
|
|
166
|
+
if (fingerprintBefore !== fingerprintAfter) {
|
|
167
|
+
// Copy corrupted the canonical content; back out.
|
|
168
|
+
safeRemove(globalDir);
|
|
169
|
+
return {
|
|
170
|
+
kind: "blocked",
|
|
171
|
+
blocker: buildMigrationUnsafeBlocker({
|
|
172
|
+
detectedAt: input.nowIso,
|
|
173
|
+
legacyPath: legacy.legacyDir,
|
|
174
|
+
reason: "copy produced non-matching fingerprint; aborted",
|
|
175
|
+
}),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const renamedPath = interruptedOrMigratedPath(legacy.legacyDir, "migrated", input.nowIso);
|
|
180
|
+
|
|
181
|
+
const record: UltraPlanSessionMigrationRecord = {
|
|
182
|
+
migratedAt: input.nowIso,
|
|
183
|
+
legacyPath: legacy.legacyDir,
|
|
184
|
+
fingerprintBefore,
|
|
185
|
+
fingerprintAfter,
|
|
186
|
+
legacyRenamedTo: renamedPath,
|
|
187
|
+
kind: "copied",
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const saved = saveMigrationRecord(input.paths, input.cwd, input.sessionId, record);
|
|
191
|
+
if (!saved.ok) {
|
|
192
|
+
safeRemove(globalDir);
|
|
193
|
+
return {
|
|
194
|
+
kind: "blocked",
|
|
195
|
+
blocker: buildMigrationUnsafeBlocker({
|
|
196
|
+
detectedAt: input.nowIso,
|
|
197
|
+
legacyPath: legacy.legacyDir,
|
|
198
|
+
reason: `migration.json write failed: ${saved.error.message}`,
|
|
199
|
+
}),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
fs.renameSync(legacy.legacyDir, renamedPath);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
return {
|
|
207
|
+
kind: "blocked",
|
|
208
|
+
blocker: buildMigrationUnsafeBlocker({
|
|
209
|
+
detectedAt: input.nowIso,
|
|
210
|
+
legacyPath: legacy.legacyDir,
|
|
211
|
+
reason: `legacy rename failed: ${formatError(error)}`,
|
|
212
|
+
}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { kind: "migrated-copied", record };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Inspection helpers
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
function inspectGlobal(input: ResolveSessionMigrationInput): GlobalState {
|
|
224
|
+
const globalDir = getUltraplanSessionDir(input.paths, input.cwd, input.sessionId);
|
|
225
|
+
if (!fs.existsSync(globalDir)) {
|
|
226
|
+
return { exists: false, authored: null, manifest: null, hasMigrationJson: false, migrationJsonValid: false };
|
|
227
|
+
}
|
|
228
|
+
const authored = readValidatedAuthored(getUltraplanAuthoredJsonPath(input.paths, input.cwd, input.sessionId));
|
|
229
|
+
const manifest = readValidatedManifest(getUltraplanManifestPath(input.paths, input.cwd, input.sessionId));
|
|
230
|
+
const migrationPath = getUltraplanMigrationRecordPath(input.paths, input.cwd, input.sessionId);
|
|
231
|
+
const hasMigrationJson = fs.existsSync(migrationPath);
|
|
232
|
+
let migrationJsonValid = false;
|
|
233
|
+
if (hasMigrationJson) {
|
|
234
|
+
try {
|
|
235
|
+
const raw = JSON.parse(fs.readFileSync(migrationPath, "utf8"));
|
|
236
|
+
migrationJsonValid = validateUltraPlanSessionMigrationRecord(raw).ok;
|
|
237
|
+
} catch {
|
|
238
|
+
migrationJsonValid = false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
exists: true,
|
|
243
|
+
authored,
|
|
244
|
+
manifest,
|
|
245
|
+
hasMigrationJson,
|
|
246
|
+
migrationJsonValid,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function inspectLegacy(input: ResolveSessionMigrationInput): LegacyState {
|
|
251
|
+
const legacyDir = getLegacyUltraplanSessionDir(input.cwd, input.sessionId);
|
|
252
|
+
if (!fs.existsSync(legacyDir)) {
|
|
253
|
+
return { exists: false, authored: null, manifest: null, canonical: false, legacyDir };
|
|
254
|
+
}
|
|
255
|
+
const authored = readValidatedAuthored(path.join(legacyDir, ULTRAPLAN_AUTHORED_JSON_FILENAME));
|
|
256
|
+
const manifest = readValidatedManifest(path.join(legacyDir, ULTRAPLAN_MANIFEST_FILENAME));
|
|
257
|
+
const canonical = authored !== null && manifest !== null;
|
|
258
|
+
return { exists: true, authored, manifest, canonical, legacyDir };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isGlobalCanonical(global: GlobalState, legacy: LegacyState): boolean {
|
|
262
|
+
if (!global.exists) return false;
|
|
263
|
+
if (!global.authored || !global.manifest) return false;
|
|
264
|
+
// Either no legacy copy (native), or migration.json present and valid (migrated).
|
|
265
|
+
if (!legacy.exists) return true;
|
|
266
|
+
return global.hasMigrationJson && global.migrationJsonValid;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Filesystem helpers
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
function readValidatedAuthored(filePath: string): UltraPlanAuthoredArtifact | null {
|
|
274
|
+
if (!fs.existsSync(filePath)) return null;
|
|
275
|
+
try {
|
|
276
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
277
|
+
const result = validateUltraPlanAuthoredArtifact(raw);
|
|
278
|
+
return result.ok ? result.value : null;
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function readValidatedManifest(filePath: string): UltraPlanManifest | null {
|
|
285
|
+
if (!fs.existsSync(filePath)) return null;
|
|
286
|
+
try {
|
|
287
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
288
|
+
const result = validateUltraPlanManifest(raw);
|
|
289
|
+
return result.ok ? result.value : null;
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function copyDirectoryTree(src: string, dest: string): void {
|
|
296
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
297
|
+
const from = path.join(src, entry.name);
|
|
298
|
+
const to = path.join(dest, entry.name);
|
|
299
|
+
if (entry.isDirectory()) {
|
|
300
|
+
fs.mkdirSync(to, { recursive: true });
|
|
301
|
+
copyDirectoryTree(from, to);
|
|
302
|
+
} else if (entry.isFile()) {
|
|
303
|
+
fs.copyFileSync(from, to);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function safeRemove(target: string): void {
|
|
309
|
+
try {
|
|
310
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
311
|
+
} catch {
|
|
312
|
+
// best effort cleanup; the caller is already in a blocked state
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function interruptedOrMigratedPath(source: string, suffix: "migrated" | "interrupted", nowIso: string): string {
|
|
317
|
+
const safeTimestamp = nowIso.replace(/[:.]/g, "-");
|
|
318
|
+
return `${source}.${suffix}-${safeTimestamp}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function formatError(error: unknown): string {
|
|
322
|
+
return error instanceof Error ? error.message : String(error);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// Fingerprinting
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
function fingerprintLegacyArtifacts(authored: UltraPlanAuthoredArtifact, manifest: UltraPlanManifest): string {
|
|
330
|
+
return fingerprintArtifacts(authored, manifest);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function fingerprintGlobalArtifacts(input: ResolveSessionMigrationInput): string {
|
|
334
|
+
const authored = readValidatedAuthored(getUltraplanAuthoredJsonPath(input.paths, input.cwd, input.sessionId));
|
|
335
|
+
const manifest = readValidatedManifest(getUltraplanManifestPath(input.paths, input.cwd, input.sessionId));
|
|
336
|
+
if (!authored || !manifest) {
|
|
337
|
+
// Produce a distinct, non-canonical sentinel so the caller can detect the mismatch.
|
|
338
|
+
return `sha256:incomplete-global-${Date.now()}`;
|
|
339
|
+
}
|
|
340
|
+
return fingerprintArtifacts(authored, manifest);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function fingerprintArtifacts(authored: UltraPlanAuthoredArtifact, manifest: UltraPlanManifest): string {
|
|
344
|
+
const canonical = JSON.stringify({
|
|
345
|
+
authored: canonicalize(authored),
|
|
346
|
+
manifest: canonicalize(manifest),
|
|
347
|
+
});
|
|
348
|
+
return `sha256:${createHash("sha256").update(canonical).digest("hex")}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function canonicalize(value: unknown): unknown {
|
|
352
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
353
|
+
if (value && typeof value === "object") {
|
|
354
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
355
|
+
.filter(([, v]) => v !== undefined)
|
|
356
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
357
|
+
.map(([k, v]) => [k, canonicalize(v)] as const);
|
|
358
|
+
return Object.fromEntries(entries);
|
|
359
|
+
}
|
|
360
|
+
return value;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
function reconcileSameContent(
|
|
365
|
+
input: ResolveSessionMigrationInput,
|
|
366
|
+
legacy: LegacyState,
|
|
367
|
+
fingerprint: string,
|
|
368
|
+
): MigrationOutcome {
|
|
369
|
+
const migrationPath = getUltraplanMigrationRecordPath(input.paths, input.cwd, input.sessionId);
|
|
370
|
+
const renamedPath = interruptedOrMigratedPath(legacy.legacyDir, "migrated", input.nowIso);
|
|
371
|
+
|
|
372
|
+
const record: UltraPlanSessionMigrationRecord = {
|
|
373
|
+
migratedAt: input.nowIso,
|
|
374
|
+
legacyPath: legacy.legacyDir,
|
|
375
|
+
fingerprintBefore: fingerprint,
|
|
376
|
+
fingerprintAfter: fingerprint,
|
|
377
|
+
legacyRenamedTo: renamedPath,
|
|
378
|
+
kind: "reconciled-no-op",
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// Branch 3 semantics: ensure migration.json exists. If it already does, leave it as-is (it was
|
|
382
|
+
// written by a prior migration). Otherwise write a fresh reconciled-no-op record.
|
|
383
|
+
let persistedRecord: UltraPlanSessionMigrationRecord = record;
|
|
384
|
+
if (fs.existsSync(migrationPath)) {
|
|
385
|
+
try {
|
|
386
|
+
const raw = JSON.parse(fs.readFileSync(migrationPath, "utf8"));
|
|
387
|
+
const validation = validateUltraPlanSessionMigrationRecord(raw);
|
|
388
|
+
if (validation.ok) {
|
|
389
|
+
persistedRecord = validation.value;
|
|
390
|
+
}
|
|
391
|
+
} catch {
|
|
392
|
+
// Fall through and write a fresh record below.
|
|
393
|
+
}
|
|
394
|
+
// If the existing file is invalid JSON or schema-invalid, overwrite with a fresh record.
|
|
395
|
+
if (persistedRecord === record) {
|
|
396
|
+
const saved = saveMigrationRecord(input.paths, input.cwd, input.sessionId, record);
|
|
397
|
+
if (!saved.ok) {
|
|
398
|
+
return {
|
|
399
|
+
kind: "blocked",
|
|
400
|
+
blocker: buildMigrationUnsafeBlocker({
|
|
401
|
+
detectedAt: input.nowIso,
|
|
402
|
+
legacyPath: legacy.legacyDir,
|
|
403
|
+
reason: `reconciled migration.json write failed: ${saved.error.message}`,
|
|
404
|
+
}),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
const saved = saveMigrationRecord(input.paths, input.cwd, input.sessionId, record);
|
|
410
|
+
if (!saved.ok) {
|
|
411
|
+
return {
|
|
412
|
+
kind: "blocked",
|
|
413
|
+
blocker: buildMigrationUnsafeBlocker({
|
|
414
|
+
detectedAt: input.nowIso,
|
|
415
|
+
legacyPath: legacy.legacyDir,
|
|
416
|
+
reason: `reconciled migration.json write failed: ${saved.error.message}`,
|
|
417
|
+
}),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
fs.renameSync(legacy.legacyDir, renamedPath);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
return {
|
|
426
|
+
kind: "blocked",
|
|
427
|
+
blocker: buildMigrationUnsafeBlocker({
|
|
428
|
+
detectedAt: input.nowIso,
|
|
429
|
+
legacyPath: legacy.legacyDir,
|
|
430
|
+
reason: `legacy rename failed during reconciliation: ${formatError(error)}`,
|
|
431
|
+
}),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return { kind: "reconciled-no-op", record: persistedRecord };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function sameCursor(a: UltraPlanManifest["cursor"], b: UltraPlanManifest["cursor"]): boolean {
|
|
439
|
+
if (a === null && b === null) return true;
|
|
440
|
+
if (a === null || b === null) return false;
|
|
441
|
+
return a.targetType === b.targetType
|
|
442
|
+
&& a.stack === b.stack
|
|
443
|
+
&& a.domainId === b.domainId
|
|
444
|
+
&& a.level === b.level
|
|
445
|
+
&& a.scenarioId === b.scenarioId
|
|
446
|
+
&& a.phase === b.phase
|
|
447
|
+
&& a.status === b.status;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
// Branch 5 — partial global, valid legacy: rename partial global then migrate from legacy.
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
function recoverFromPartialGlobal(
|
|
455
|
+
input: ResolveSessionMigrationInput,
|
|
456
|
+
legacy: LegacyState,
|
|
457
|
+
): MigrationOutcome {
|
|
458
|
+
const globalDir = getUltraplanSessionDir(input.paths, input.cwd, input.sessionId);
|
|
459
|
+
const interruptedPath = interruptedOrMigratedPath(globalDir, "interrupted", input.nowIso);
|
|
460
|
+
try {
|
|
461
|
+
fs.renameSync(globalDir, interruptedPath);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
return {
|
|
464
|
+
kind: "blocked",
|
|
465
|
+
blocker: buildMigrationUnsafeBlocker({
|
|
466
|
+
detectedAt: input.nowIso,
|
|
467
|
+
legacyPath: legacy.legacyDir,
|
|
468
|
+
reason: `failed to rename partial global directory: ${formatError(error)}`,
|
|
469
|
+
interruptedPath,
|
|
470
|
+
}),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
// Re-run branch 6 against the now-absent global directory.
|
|
474
|
+
return migrateFromLegacy(input, legacy);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ---------------------------------------------------------------------------
|
|
478
|
+
// Branch 7 — non-canonical global, no legacy: rename and emit migration-unsafe blocker.
|
|
479
|
+
// ---------------------------------------------------------------------------
|
|
480
|
+
|
|
481
|
+
function classifyOrphanedGlobal(input: ResolveSessionMigrationInput): MigrationOutcome {
|
|
482
|
+
const globalDir = getUltraplanSessionDir(input.paths, input.cwd, input.sessionId);
|
|
483
|
+
const interruptedPath = interruptedOrMigratedPath(globalDir, "interrupted", input.nowIso);
|
|
484
|
+
try {
|
|
485
|
+
fs.renameSync(globalDir, interruptedPath);
|
|
486
|
+
return {
|
|
487
|
+
kind: "blocked",
|
|
488
|
+
blocker: buildMigrationUnsafeBlocker({
|
|
489
|
+
detectedAt: input.nowIso,
|
|
490
|
+
legacyPath: getLegacyUltraplanSessionDir(input.cwd, input.sessionId),
|
|
491
|
+
reason: "global session directory is not canonical and no legacy copy is available",
|
|
492
|
+
interruptedPath,
|
|
493
|
+
}),
|
|
494
|
+
};
|
|
495
|
+
} catch (error) {
|
|
496
|
+
return {
|
|
497
|
+
kind: "blocked",
|
|
498
|
+
blocker: buildMigrationUnsafeBlocker({
|
|
499
|
+
detectedAt: input.nowIso,
|
|
500
|
+
legacyPath: getLegacyUltraplanSessionDir(input.cwd, input.sessionId),
|
|
501
|
+
reason: `failed to rename non-canonical global directory: ${formatError(error)}`,
|
|
502
|
+
}),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function describeContentMismatch(
|
|
508
|
+
global: GlobalState,
|
|
509
|
+
legacy: LegacyState,
|
|
510
|
+
fingerprintGlobal: string,
|
|
511
|
+
fingerprintLegacy: string,
|
|
512
|
+
): string {
|
|
513
|
+
const reasons: string[] = [];
|
|
514
|
+
if (global.manifest?.updatedAt !== legacy.manifest?.updatedAt) {
|
|
515
|
+
reasons.push(`updatedAt differs (global=${global.manifest?.updatedAt}, legacy=${legacy.manifest?.updatedAt})`);
|
|
516
|
+
}
|
|
517
|
+
if (!sameCursor(global.manifest?.cursor ?? null, legacy.manifest?.cursor ?? null)) {
|
|
518
|
+
reasons.push("cursor differs");
|
|
519
|
+
}
|
|
520
|
+
if (fingerprintGlobal !== fingerprintLegacy) {
|
|
521
|
+
reasons.push("authored/manifest fingerprints differ");
|
|
522
|
+
}
|
|
523
|
+
return reasons.length > 0 ? reasons.join("; ") : "contents differ";
|
|
524
|
+
}
|