@khoavu882/doflow 2.0.0-beta.3 → 2.0.0-beta.5
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/bin/doflow.js +2 -2
- package/core/.antigravity-plugin/plugin.json +0 -1
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
- package/core/harnesses/claude/hooks/session-start.sh +5 -3
- package/core/harnesses/claude/hooks/stop-check.sh +5 -3
- package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
- package/core/harnesses/codex/hooks/session-start.sh +5 -3
- package/core/harnesses/codex/hooks/stop-check.sh +6 -5
- package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
- package/core/harnesses/gemini/hooks/session-start.sh +5 -3
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
- package/core/harnesses/kiro/hooks/session-start.sh +11 -5
- package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
- package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
- package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
- package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
- package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
- package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
- package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
- package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
- package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
- package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
- package/core/registry/assets.json +19 -0
- package/core/registry/contracts.json +2 -2
- package/core/registry/harnesses.json +196 -10
- package/core/registry/lifecycle.json +2626 -7
- package/core/registry/storage.json +110 -83
- package/core/registry/verification.json +1 -1
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
- package/core/shared/scripts/doflow/bin/doflow-run +10 -6
- package/core/shared/skills/do-design/SKILL.md +6 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
- package/core/shared/skills/do-execute-plan/references/scaffold.md +2 -2
- package/core/shared/skills/do-implement/SKILL.md +3 -2
- package/core/shared/skills/do-plan/SKILL.md +7 -8
- package/package.json +4 -2
- package/src/adapters/antigravity/{index.ts → index.js} +141 -105
- package/src/adapters/claude/{index.ts → index.js} +63 -49
- package/src/adapters/claude/{mcp.ts → mcp.js} +16 -16
- package/src/adapters/codex/{agents.ts → agents.js} +24 -24
- package/src/adapters/codex/{config.ts → config.js} +49 -49
- package/src/adapters/codex/{hooks.ts → hooks.js} +62 -27
- package/src/adapters/codex/{index.ts → index.js} +148 -105
- package/src/adapters/codex/{mcp.ts → mcp.js} +38 -38
- package/src/adapters/codex/{native-projection.ts → native-projection.js} +5 -5
- package/src/adapters/copilot/{index.ts → index.js} +42 -42
- package/src/adapters/{copy-tree.ts → copy-tree.js} +74 -74
- package/src/adapters/gemini/{hooks.ts → hooks.js} +78 -37
- package/src/adapters/gemini/{index.ts → index.js} +96 -57
- package/src/adapters/{hook-commands.ts → hook-commands.js} +8 -8
- package/src/adapters/{index.ts → index.js} +13 -13
- package/src/adapters/kiro/{index.ts → index.js} +49 -35
- package/src/adapters/{mcp-entries.ts → mcp-entries.js} +55 -55
- package/src/adapters/opencode/{index.ts → index.js} +52 -52
- package/src/adapters/pi/hooks.js +119 -0
- package/src/adapters/pi/{index.ts → index.js} +64 -39
- package/src/adapters/pi/{json-members.ts → json-members.js} +23 -23
- package/src/adapters/pi/{mcp.ts → mcp.js} +70 -70
- package/src/adapters/{resolve-source.ts → resolve-source.js} +1 -1
- package/src/cli/commands/{install.ts → install.js} +25 -25
- package/src/cli/commands/{list-backups.ts → list-backups.js} +4 -4
- package/src/cli/commands/{reconcile.ts → reconcile.js} +19 -19
- package/src/cli/commands/{remove.ts → remove.js} +10 -10
- package/src/cli/commands/{rollback.ts → rollback.js} +17 -17
- package/src/cli/commands/{self-update.ts → self-update.js} +3 -3
- package/src/cli/commands/{status.ts → status.js} +24 -24
- package/src/cli/commands/{tools.ts → tools.js} +16 -16
- package/src/cli/commands/{update.ts → update.js} +20 -20
- package/src/cli/{index.ts → index.js} +43 -31
- package/src/cli/{runtime-commands.ts → runtime-commands.js} +97 -58
- package/src/cli/{shared.ts → shared.js} +40 -40
- package/src/helper/{bash-helper.ts → bash-helper.js} +7 -7
- package/src/helper/{git.ts → git.js} +2 -2
- package/src/helper/{guardrails.ts → guardrails.js} +10 -10
- package/src/helper/{harness-paths.ts → harness-paths.js} +24 -24
- package/src/helper/{marker-merge.ts → marker-merge.js} +4 -4
- package/src/helper/{prompt.ts → prompt.js} +5 -5
- package/src/helper/{repo-root.ts → repo-root.js} +2 -2
- package/src/helper/{settings-merge.ts → settings-merge.js} +27 -27
- package/src/helper/{settings-scope.ts → settings-scope.js} +3 -3
- package/src/helper/{toml.ts → toml.js} +48 -48
- package/src/install/{backup-set.ts → backup-set.js} +16 -16
- package/src/install/{backup.ts → backup.js} +65 -65
- package/src/install/{context.ts → context.js} +15 -15
- package/src/install/{manifest.ts → manifest.js} +27 -27
- package/src/install/{mcp.ts → mcp.js} +30 -30
- package/src/install/{paths.ts → paths.js} +15 -15
- package/src/install/{targets.ts → targets.js} +10 -10
- package/src/install/{tool-lifecycle.ts → tool-lifecycle.js} +20 -20
- package/src/lifecycle/{bash-availability.ts → bash-availability.js} +1 -1
- package/src/lifecycle/coverage.js +57 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/{index.ts → index.js} +160 -146
- package/src/lifecycle/{mcp-index.ts → mcp-index.js} +1 -1
- package/src/lifecycle/{policies.ts → policies.js} +11 -11
- package/src/lifecycle/{view.ts → view.js} +28 -28
- package/src/registry/{index.ts → index.js} +195 -66
- package/src/release/build-package.js +91 -0
- package/src/release/changelog-section.js +70 -0
- package/src/release/{sync-plugin-versions.ts → sync-plugin-versions.js} +4 -4
- package/src/runtime/advice/{cli.ts → cli.js} +31 -31
- package/src/runtime/advice/{client.ts → client.js} +44 -44
- package/src/runtime/advice/{config.ts → config.js} +16 -16
- package/src/runtime/advice/{doctor.ts → doctor.js} +32 -32
- package/src/runtime/advice/{endpoint-guard.ts → endpoint-guard.js} +20 -20
- package/src/runtime/advice/{kinds.ts → kinds.js} +16 -16
- package/src/runtime/advice/providers/{ollama-systemone.ts → ollama-systemone.js} +21 -21
- package/src/runtime/advice/{record.ts → record.js} +19 -19
- package/src/runtime/advice/{registry.ts → registry.js} +16 -16
- package/src/runtime/advice/{strength.ts → strength.js} +7 -7
- package/src/runtime/{brief-tools.ts → brief-tools.js} +50 -50
- package/src/runtime/{c4-project.ts → c4-project.js} +27 -27
- package/src/runtime/candidate.js +505 -0
- package/src/runtime/{capability-router.ts → capability-router.js} +39 -39
- package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
- package/src/runtime/{claims.ts → claims.js} +44 -35
- package/src/runtime/{cli-result.ts → cli-result.js} +15 -8
- package/src/runtime/{cli.ts → cli.js} +59 -292
- package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
- package/src/runtime/context/budget.js +203 -0
- package/src/runtime/context/compose.js +1367 -0
- package/src/runtime/context/delivery-store.js +102 -0
- package/src/runtime/context/mandatory.js +594 -0
- package/src/runtime/context/pointer.js +142 -0
- package/src/runtime/context-pack.js +1277 -0
- package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
- package/src/runtime/evidence-fields.js +39 -0
- package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +32 -25
- package/src/runtime/failure/{capture.ts → capture.js} +29 -29
- package/src/runtime/failure/{classifier.ts → classifier.js} +4 -4
- package/src/runtime/failure/{cli.ts → cli.js} +30 -30
- package/src/runtime/failure/{home.ts → home.js} +6 -6
- package/src/runtime/failure/{store.ts → store.js} +32 -32
- package/src/runtime/{feature-resolve.ts → feature-resolve.js} +6 -6
- package/src/runtime/{freshness.ts → freshness.js} +59 -22
- package/src/runtime/{health.ts → health.js} +72 -72
- package/src/runtime/{history-compactor.ts → history-compactor.js} +25 -25
- package/src/runtime/{implementation-gate.ts → implementation-gate.js} +30 -30
- package/src/runtime/inventory/{coverage.ts → coverage.js} +10 -10
- package/src/runtime/inventory/{identity.ts → identity.js} +2 -2
- package/src/runtime/inventory/{index.ts → index.js} +36 -36
- package/src/runtime/inventory/{precedence.ts → precedence.js} +2 -2
- package/src/runtime/inventory/{read-scopes.ts → read-scopes.js} +9 -9
- package/src/runtime/inventory/{siblings.ts → siblings.js} +15 -15
- package/src/runtime/knowledge/{chunker.ts → chunker.js} +12 -12
- package/src/runtime/knowledge/{hybrid.ts → hybrid.js} +17 -17
- package/src/runtime/knowledge/{index-store.ts → index-store.js} +23 -23
- package/src/runtime/knowledge/{retrieval.ts → retrieval.js} +27 -27
- package/src/runtime/{leak-scan.ts → leak-scan.js} +12 -12
- package/src/runtime/lifecycle/{cli.ts → cli.js} +32 -32
- package/src/runtime/lifecycle/{event-store.ts → event-store.js} +48 -48
- package/src/runtime/lifecycle/{fold.ts → fold.js} +41 -41
- package/src/runtime/lifecycle/{followup.ts → followup.js} +54 -54
- package/src/runtime/lifecycle/{goal.ts → goal.js} +50 -50
- package/src/runtime/lifecycle/{intent-writer.ts → intent-writer.js} +5 -5
- package/src/runtime/lifecycle/{overview.ts → overview.js} +47 -47
- package/src/runtime/lifecycle/{release.ts → release.js} +29 -29
- package/src/runtime/lifecycle/{report-store.ts → report-store.js} +25 -25
- package/src/runtime/lifecycle/{retention.ts → retention.js} +37 -37
- package/src/runtime/lifecycle/{root.ts → root.js} +9 -9
- package/src/runtime/lifecycle/{status.ts → status.js} +64 -64
- package/src/runtime/lifecycle/{store-upkeep.ts → store-upkeep.js} +46 -46
- package/src/runtime/{locator-resolve.ts → locator-resolve.js} +7 -7
- package/src/runtime/{mask.ts → mask.js} +22 -22
- package/src/runtime/{mcp-consultations.ts → mcp-consultations.js} +54 -54
- package/src/runtime/{mcp-reach.ts → mcp-reach.js} +24 -24
- package/src/runtime/{model-router.ts → model-router.js} +20 -20
- package/src/runtime/{outcome.ts → outcome.js} +27 -27
- package/src/runtime/{reach.ts → reach.js} +21 -21
- package/src/runtime/{readiness-record.ts → readiness-record.js} +12 -12
- package/src/runtime/{readiness.ts → readiness.js} +113 -43
- package/src/runtime/{recovery.ts → recovery.js} +15 -15
- package/src/runtime/{research-request.ts → research-request.js} +34 -34
- package/src/runtime/{retrieval-plan.ts → retrieval-plan.js} +453 -104
- package/src/runtime/retrieval-record.js +223 -0
- package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +22 -19
- package/src/runtime/scaffold/{fingerprint.ts → fingerprint.js} +3 -3
- package/src/runtime/scaffold/{generate.ts → generate.js} +109 -109
- package/src/runtime/scaffold/{languages.ts → languages.js} +34 -34
- package/src/runtime/source-version.js +289 -0
- package/src/runtime/storage/{cli.ts → cli.js} +9 -9
- package/src/runtime/storage/{engine.ts → engine.js} +37 -37
- package/src/runtime/storage/{registry.ts → registry.js} +19 -19
- package/src/runtime/storage/{roots.ts → roots.js} +8 -8
- package/src/runtime/storage/{rules.ts → rules.js} +26 -26
- package/src/runtime/storage/{upkeep.ts → upkeep.js} +35 -35
- package/src/runtime/storage/{work-status.ts → work-status.js} +39 -39
- package/src/runtime/{task-attempts.ts → task-attempts.js} +62 -62
- package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
- package/src/runtime/{task-scope.ts → task-scope.js} +40 -15
- package/src/runtime/{task-state.ts → task-state.js} +86 -21
- package/src/runtime/trace/{indicators.ts → indicators.js} +22 -22
- package/src/runtime/trace/{ledger.ts → ledger.js} +42 -42
- package/src/runtime/trace/{render.ts → render.js} +7 -7
- package/src/runtime/trace/{views.ts → views.js} +40 -40
- package/src/runtime/verification/{contract-runner.ts → contract-runner.js} +23 -23
- package/src/runtime/verification/{engine.ts → engine.js} +143 -143
- package/src/runtime/verification/{registry.ts → registry.js} +6 -6
- package/src/runtime/verification/{scope-bound.ts → scope-bound.js} +50 -50
- package/src/runtime/verification/{test-selection.ts → test-selection.js} +28 -28
- package/src/runtime/{workflow-engine.ts → workflow-engine.js} +50 -50
- package/src/runtime/{workflow-orchestrator.ts → workflow-orchestrator.js} +91 -91
- package/src/runtime/{worktree.ts → worktree.js} +31 -31
- package/src/state/{index.ts → index.js} +26 -26
- package/src/state/{lockfile.ts → lockfile.js} +20 -20
- package/src/state/{run-lock.ts → run-lock.js} +44 -44
- package/core/.antigravity-plugin/hooks.json +0 -39
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
- package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
- package/core/harnesses/shared/hooks/hooks.json +0 -47
- package/src/runtime/context-pack.ts +0 -492
|
@@ -37,9 +37,9 @@ const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/;
|
|
|
37
37
|
* are true when a fence or an HTML comment is never closed, so everything after it was swallowed;
|
|
38
38
|
* `openAt` is the line index where that construct opened (-1 when none is left open)
|
|
39
39
|
*/
|
|
40
|
-
function scan(lines
|
|
41
|
-
const kinds
|
|
42
|
-
let fence
|
|
40
|
+
function scan(lines ) {
|
|
41
|
+
const kinds = new Array(lines.length);
|
|
42
|
+
let fence = null;
|
|
43
43
|
let comment = false;
|
|
44
44
|
let openedAt = -1;
|
|
45
45
|
for (let i = 0; i < lines.length; i += 1) {
|
|
@@ -73,14 +73,14 @@ function scan(lines: string[]): { kinds: string[]; openFence: boolean; openComme
|
|
|
73
73
|
* exact canonical pointer for this artifact is the pointer; any other line, including one that
|
|
74
74
|
* merely looks like a pointer, moves, so no text is dropped.
|
|
75
75
|
*/
|
|
76
|
-
function classify(bodyLines
|
|
77
|
-
const comments
|
|
78
|
-
const moved
|
|
79
|
-
let pointer
|
|
80
|
-
let block
|
|
76
|
+
function classify(bodyLines , kinds , canonicalPointer ) {
|
|
77
|
+
const comments = [];
|
|
78
|
+
const moved = [];
|
|
79
|
+
let pointer = null;
|
|
80
|
+
let block = null;
|
|
81
81
|
bodyLines.forEach((line, i) => {
|
|
82
82
|
const kind = kinds[i];
|
|
83
|
-
if (kind === 'comment-cont') { (block
|
|
83
|
+
if (kind === 'comment-cont') { (block ).push(line); return; }
|
|
84
84
|
block = null;
|
|
85
85
|
if (kind === 'comment') { block = [line]; comments.push(block); return; }
|
|
86
86
|
if (kind === 'fence') { moved.push(line); return; }
|
|
@@ -93,19 +93,19 @@ function classify(bodyLines: string[], kinds: string[], canonicalPointer: string
|
|
|
93
93
|
return { comments, moved, pointer };
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
function writeAtomic(fsImpl
|
|
96
|
+
function writeAtomic(fsImpl , file , content ) {
|
|
97
97
|
fsImpl.mkdirSync(path.dirname(file), { recursive: true });
|
|
98
98
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
99
99
|
try {
|
|
100
100
|
fsImpl.writeFileSync(tmp, content, 'utf8');
|
|
101
101
|
fsImpl.renameSync(tmp, file);
|
|
102
|
-
} catch (error
|
|
102
|
+
} catch (error ) {
|
|
103
103
|
try { fsImpl.rmSync(tmp, { force: true }); } catch { /* nothing to clean */ }
|
|
104
104
|
throw error;
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
function toPosix(p
|
|
108
|
+
function toPosix(p ) {
|
|
109
109
|
return p.split(path.sep).join('/');
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -119,7 +119,7 @@ function toPosix(p: string): string {
|
|
|
119
119
|
* a line inside a code fence, and that must not be read as the start of a chunk. The dedupe and the
|
|
120
120
|
* post-write confirmation both use this one predicate.
|
|
121
121
|
*/
|
|
122
|
-
function lastChunkIs(archiveText
|
|
122
|
+
function lastChunkIs(archiveText , blockLF ) {
|
|
123
123
|
const lines = archiveText.replace(/\r\n/g, '\n').split('\n');
|
|
124
124
|
const { kinds } = scan(lines);
|
|
125
125
|
let header = -1;
|
|
@@ -135,25 +135,25 @@ function lastChunkIs(archiveText: string, blockLF: string): boolean {
|
|
|
135
135
|
* @returns {{artifact:string, path:string, archive:string, lines:number}|null} null when there was
|
|
136
136
|
* nothing to move
|
|
137
137
|
*/
|
|
138
|
-
function compactArtifact({ fsImpl, featureDir, file, date }
|
|
138
|
+
function compactArtifact({ fsImpl, featureDir, file, date } ) {
|
|
139
139
|
const text = fsImpl.readFileSync(file, 'utf8');
|
|
140
140
|
const crlf = text.includes('\r\n');
|
|
141
141
|
const lines = text.split('\n');
|
|
142
142
|
const { kinds, openFence, openComment, openAt } = scan(lines);
|
|
143
|
-
const unclosed
|
|
144
|
-
const refuse = ()
|
|
143
|
+
const unclosed = openFence ? 'a code fence' : openComment ? 'an HTML comment' : null;
|
|
144
|
+
const refuse = () => {
|
|
145
145
|
throw new Error(`${unclosed} is never closed, so the end of the History section cannot be told`);
|
|
146
146
|
};
|
|
147
|
-
const start = lines.findIndex((l
|
|
147
|
+
const start = lines.findIndex((l , i ) => kinds[i] === 'text' && HISTORY_HEADING.test(l));
|
|
148
148
|
// An unclosed construct swallows everything after it. It matters only when it opens before the
|
|
149
149
|
// History section ends (FR-001): then the section's end cannot be told. A History heading it
|
|
150
150
|
// swallowed is such a case; one that simply is not there, or that closed before the construct
|
|
151
151
|
// opened, is not, and the construct is treated as absent.
|
|
152
152
|
if (start === -1) {
|
|
153
|
-
if (unclosed && lines.some((l
|
|
153
|
+
if (unclosed && lines.some((l , i ) => i > openAt && HISTORY_HEADING.test(l))) refuse();
|
|
154
154
|
return null;
|
|
155
155
|
}
|
|
156
|
-
const section = Number((HISTORY_HEADING.exec(lines[start])
|
|
156
|
+
const section = Number((HISTORY_HEADING.exec(lines[start]) )[1]);
|
|
157
157
|
let end = lines.length;
|
|
158
158
|
for (let i = start + 1; i < lines.length; i += 1) {
|
|
159
159
|
if (kinds[i] === 'text' && ANY_H2.test(lines[i])) { end = i; break; }
|
|
@@ -172,7 +172,7 @@ function compactArtifact({ fsImpl, featureDir, file, date }: { fsImpl: any; feat
|
|
|
172
172
|
// Chunks are built with LF and converted to the artifact's own line ending on write, so a CRLF
|
|
173
173
|
// artifact gets a CRLF archive and no file ends up mixed.
|
|
174
174
|
const eol = crlf ? '\r\n' : '\n';
|
|
175
|
-
const toEol = (t
|
|
175
|
+
const toEol = (t ) => (crlf ? t.replace(/\n/g, '\r\n') : t);
|
|
176
176
|
const blockLF = moved.map((l) => l.replace(/\r$/, '')).join('\n');
|
|
177
177
|
const chunk = `## Compacted ${date} from ${rel} §${section}\n\n${blockLF}\n`;
|
|
178
178
|
const existing = fsImpl.existsSync(archiveFile) ? fsImpl.readFileSync(archiveFile, 'utf8') : null;
|
|
@@ -213,18 +213,18 @@ function compactArtifact({ fsImpl, featureDir, file, date }: { fsImpl: any; feat
|
|
|
213
213
|
* @returns {{status:'compacted'|'unchanged'|'partial', moved:Object[], failed:Array<{artifact:string, path:string, message:string}>}}
|
|
214
214
|
* `partial` means at least one artifact failed
|
|
215
215
|
*/
|
|
216
|
-
function compactHistory({ repoRoot, featureDir, paths, date, fsImpl = nodeFs }
|
|
216
|
+
function compactHistory({ repoRoot, featureDir, paths, date, fsImpl = nodeFs } ) {
|
|
217
217
|
const stamp = date || new Date().toISOString().slice(0, 10);
|
|
218
|
-
const moved
|
|
219
|
-
const failed
|
|
218
|
+
const moved = [];
|
|
219
|
+
const failed = [];
|
|
220
220
|
for (const key of TARGET_KEYS) {
|
|
221
221
|
if (!paths || !paths[key]) continue;
|
|
222
222
|
const file = path.resolve(repoRoot, paths[key]);
|
|
223
223
|
if (!fsImpl.existsSync(file)) continue;
|
|
224
|
-
let result
|
|
224
|
+
let result ;
|
|
225
225
|
try {
|
|
226
226
|
result = compactArtifact({ fsImpl, featureDir, file, date: stamp });
|
|
227
|
-
} catch (error
|
|
227
|
+
} catch (error ) {
|
|
228
228
|
const name = toPosix(path.relative(featureDir, file));
|
|
229
229
|
failed.push({ artifact: path.basename(file), path: name, message: `could not compact ${name}: ${error.message}; the artifact was left unchanged` });
|
|
230
230
|
continue;
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('node:fs');
|
|
6
6
|
const path = require('node:path');
|
|
7
|
-
const { readReadinessRecord, newerVersionOf, RECORD_VERSION } = require('./readiness-record.
|
|
8
|
-
const { listCheckouts } = require('./checkouts.
|
|
9
|
-
const { readTaskState } = require('./task-state.
|
|
10
|
-
const { resolveActiveFeature } = require('./feature-resolve.
|
|
11
|
-
const { isSafeSlug } = require('./task-scope.
|
|
12
|
-
const { REPO_ROOT } = require('../helper/repo-root.
|
|
7
|
+
const { readReadinessRecord, newerVersionOf, RECORD_VERSION } = require('./readiness-record.js');
|
|
8
|
+
const { listCheckouts } = require('./checkouts.js');
|
|
9
|
+
const { readTaskState } = require('./task-state.js');
|
|
10
|
+
const { resolveActiveFeature } = require('./feature-resolve.js');
|
|
11
|
+
const { isSafeSlug } = require('./task-scope.js');
|
|
12
|
+
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
13
13
|
|
|
14
14
|
// The implementation gate: which stage of a workflow needs a READY readiness record before it is
|
|
15
15
|
// handed off, whether a task has one, and the words every refusal uses.
|
|
@@ -30,8 +30,8 @@ const NEWER_NEXT = 'run this command with the DoFlow that wrote the record, or u
|
|
|
30
30
|
* @param {{editTime?: boolean}} [options]
|
|
31
31
|
* @returns {{id: string, readinessTemplate: string, editTimeGate: boolean}|null}
|
|
32
32
|
*/
|
|
33
|
-
function gatedStage(stages
|
|
34
|
-
const stage = (stages || []).find((s
|
|
33
|
+
function gatedStage(stages , { editTime = false } = {}) {
|
|
34
|
+
const stage = (stages || []).find((s ) => s && s.mutatesSource === true
|
|
35
35
|
&& typeof s.readinessTemplate === 'string' && s.readinessTemplate !== ''
|
|
36
36
|
&& (!editTime || s.editTimeGate !== false));
|
|
37
37
|
return stage ? { id: stage.id, readinessTemplate: stage.readinessTemplate, editTimeGate: stage.editTimeGate !== false } : null;
|
|
@@ -43,12 +43,12 @@ function gatedStage(stages: any, { editTime = false }: any = {}) {
|
|
|
43
43
|
* @param {{editTime?: boolean}} [options]
|
|
44
44
|
* @returns {Array<string>}
|
|
45
45
|
*/
|
|
46
|
-
function heldClasses(engine
|
|
47
|
-
return engine.listClasses().filter((cls
|
|
46
|
+
function heldClasses(engine , { editTime = false } = {}) {
|
|
47
|
+
return engine.listClasses().filter((cls ) => gatedStage(engine.resolveWorkflow(cls).stages, { editTime }) !== null);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/** The command that clears every readiness refusal. */
|
|
51
|
-
function nextCommand({ taskId, slug, template }
|
|
51
|
+
function nextCommand({ taskId, slug, template } ) {
|
|
52
52
|
const withSlug = slug && slug !== taskId ? ` --slug=${slug}` : '';
|
|
53
53
|
return `doflow-run readiness --task-class ${template} --task-id ${taskId}${withSlug}`;
|
|
54
54
|
}
|
|
@@ -68,7 +68,7 @@ function nextCommand({ taskId, slug, template }: any) {
|
|
|
68
68
|
* replace such a record, so the text does not send the caller there
|
|
69
69
|
* @returns {string}
|
|
70
70
|
*/
|
|
71
|
-
function refusalText(code
|
|
71
|
+
function refusalText(code , { taskId, slug = null, template, record = null, candidates = [], detail = null, file = null, newerVersion = null } ) {
|
|
72
72
|
const next = nextCommand({ taskId, slug, template });
|
|
73
73
|
const tail = `Next: ${next}, then gather what it lists until it reports READY. Nothing was changed.`;
|
|
74
74
|
if (code === 'unusable' && newerVersion !== null) {
|
|
@@ -103,14 +103,14 @@ function refusalText(code: any, { taskId, slug = null, template, record = null,
|
|
|
103
103
|
* @param {{repoRoot?: string}} [options]
|
|
104
104
|
* @returns {string}
|
|
105
105
|
*/
|
|
106
|
-
function harnessHookNote({ repoRoot = REPO_ROOT }
|
|
106
|
+
function harnessHookNote({ repoRoot = REPO_ROOT } = {}) {
|
|
107
107
|
try {
|
|
108
108
|
const { policies } = JSON.parse(fs.readFileSync(path.join(repoRoot, 'core', 'registry', 'lifecycle.json'), 'utf8'));
|
|
109
|
-
const { mappings } = policies.find((p
|
|
109
|
+
const { mappings } = policies.find((p ) => p.id === 'pre-implementation-gate');
|
|
110
110
|
const runs = [];
|
|
111
111
|
const gated = new Map();
|
|
112
112
|
const none = [];
|
|
113
|
-
for (const [id, m] of (Object.entries(mappings)
|
|
113
|
+
for (const [id, m] of (Object.entries(mappings) )) {
|
|
114
114
|
const prerequisites = Array.isArray(m.prerequisites) ? m.prerequisites : [];
|
|
115
115
|
if (m.status === 'unavailable' || (m.status !== 'supported' && m.status !== 'different')) none.push(id);
|
|
116
116
|
else if (prerequisites.length === 0) runs.push(id);
|
|
@@ -129,17 +129,17 @@ function harnessHookNote({ repoRoot = REPO_ROOT }: any = {}): string {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
const isDir = (dir
|
|
132
|
+
const isDir = (dir ) => {
|
|
133
133
|
try {
|
|
134
134
|
return fs.statSync(dir).isDirectory();
|
|
135
135
|
} catch {
|
|
136
136
|
return false;
|
|
137
137
|
}
|
|
138
138
|
};
|
|
139
|
-
const folderOf = (root
|
|
139
|
+
const folderOf = (root , slug ) => path.join(root, 'agent-docs', 'doflow', slug);
|
|
140
140
|
|
|
141
141
|
/** The branch's feature slug as the resolver names it, or null. */
|
|
142
|
-
function branchFeatureSlug(cwd
|
|
142
|
+
function branchFeatureSlug(cwd ) {
|
|
143
143
|
try {
|
|
144
144
|
const found = resolveActiveFeature({ projectRoot: cwd });
|
|
145
145
|
const s = !found.error && found.paths ? found.paths.feature_slug : null;
|
|
@@ -155,7 +155,7 @@ function branchFeatureSlug(cwd: any) {
|
|
|
155
155
|
* @param {{projectRoot: string, taskId?: string|null, slug?: string|null}} options
|
|
156
156
|
* @returns {string|null}
|
|
157
157
|
*/
|
|
158
|
-
function featureSlugFor({ projectRoot, taskId = null, slug = null }
|
|
158
|
+
function featureSlugFor({ projectRoot, taskId = null, slug = null } ) {
|
|
159
159
|
if (slug) return slug;
|
|
160
160
|
if (isSafeSlug(taskId) && isDir(folderOf(projectRoot, taskId))) return taskId;
|
|
161
161
|
return branchFeatureSlug(projectRoot);
|
|
@@ -163,7 +163,7 @@ function featureSlugFor({ projectRoot, taskId = null, slug = null }: any) {
|
|
|
163
163
|
|
|
164
164
|
/** The feature a run belongs to: the slug recorded when it started, else its task id when that names
|
|
165
165
|
* a feature folder in the checkout holding the run. */
|
|
166
|
-
function runSlugOf(run
|
|
166
|
+
function runSlugOf(run , root ) {
|
|
167
167
|
if (run && typeof run.featureSlug === 'string' && run.featureSlug !== '') return run.featureSlug;
|
|
168
168
|
return run && isSafeSlug(run.taskId) && isDir(folderOf(root, run.taskId)) ? run.taskId : null;
|
|
169
169
|
}
|
|
@@ -176,7 +176,7 @@ function runSlugOf(run: any, root: any) {
|
|
|
176
176
|
* @returns {{status: 'found'|'missing'|'ambiguous', file: string|null, root: string|null,
|
|
177
177
|
* origin: 'current'|'other'|null, candidates: Array<string>}}
|
|
178
178
|
*/
|
|
179
|
-
function findRunFile({ stateRoot, taskId, slug, exec }
|
|
179
|
+
function findRunFile({ stateRoot, taskId, slug, exec } ) {
|
|
180
180
|
const rel = path.join('.doflow', 'state', 'orchestration', `${taskId}.json`);
|
|
181
181
|
const missing = { status: 'missing', file: null, root: null, origin: null, candidates: [] };
|
|
182
182
|
if (fs.existsSync(path.join(stateRoot, rel))) {
|
|
@@ -185,7 +185,7 @@ function findRunFile({ stateRoot, taskId, slug, exec }: any) {
|
|
|
185
185
|
if (!slug) return missing;
|
|
186
186
|
const checkouts = listCheckouts({ cwd: stateRoot, ...(exec ? { exec } : {}) });
|
|
187
187
|
if (!checkouts.ok || checkouts.sandbox) return missing;
|
|
188
|
-
const matches = checkouts.others.filter((root
|
|
188
|
+
const matches = checkouts.others.filter((root ) => {
|
|
189
189
|
const file = path.join(root, rel);
|
|
190
190
|
if (!fs.existsSync(file)) return false;
|
|
191
191
|
try {
|
|
@@ -195,13 +195,13 @@ function findRunFile({ stateRoot, taskId, slug, exec }: any) {
|
|
|
195
195
|
}
|
|
196
196
|
});
|
|
197
197
|
if (matches.length === 0) return missing;
|
|
198
|
-
if (matches.length > 1) return { ...missing, status: 'ambiguous', candidates: matches.map((root
|
|
198
|
+
if (matches.length > 1) return { ...missing, status: 'ambiguous', candidates: matches.map((root ) => path.join(root, rel)) };
|
|
199
199
|
return { status: 'found', file: path.join(matches[0], rel), root: matches[0], origin: 'other', candidates: [] };
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
/** The slug a refusal's next command names: one whose feature folder exists in this checkout or in
|
|
203
203
|
* the main checkout, which is when `readiness --slug` resolves it the same way the edit hook does. */
|
|
204
|
-
function nextSlugFor({ stateRoot, taskId, slug, exec }
|
|
204
|
+
function nextSlugFor({ stateRoot, taskId, slug, exec } ) {
|
|
205
205
|
if (!slug || slug === taskId) return null;
|
|
206
206
|
if (isDir(folderOf(stateRoot, slug))) return slug;
|
|
207
207
|
const checkouts = listCheckouts({ cwd: stateRoot, ...(exec ? { exec } : {}) });
|
|
@@ -209,7 +209,7 @@ function nextSlugFor({ stateRoot, taskId, slug, exec }: any) {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
/** Why a record cannot be read as an evaluation, or null when it can. */
|
|
212
|
-
function unusableDetail(record
|
|
212
|
+
function unusableDetail(record , now ) {
|
|
213
213
|
if (!record || typeof record !== 'object' || Array.isArray(record)) return 'not a JSON object';
|
|
214
214
|
if (record.version !== RECORD_VERSION) return `version ${JSON.stringify(record.version)}, this runtime reads ${RECORD_VERSION}`;
|
|
215
215
|
for (const key of ['state', 'taskClass', 'evaluatedAt']) {
|
|
@@ -235,11 +235,11 @@ function unusableDetail(record: any, now: any) {
|
|
|
235
235
|
* record: Object|null, file: string|null, origin: string|null, candidates: Array<string>,
|
|
236
236
|
* detail: string|null, nextSlug: string|null, message: string|null}}
|
|
237
237
|
*/
|
|
238
|
-
function checkReadiness({ stateRoot, taskId, slug = null, template, now = new Date(), exec }
|
|
238
|
+
function checkReadiness({ stateRoot, taskId, slug = null, template, now = new Date(), exec } ) {
|
|
239
239
|
const read = readReadinessRecord({ stateRoot, taskId, slug, exec });
|
|
240
240
|
const newerVersion = read.status === 'found' ? newerVersionOf(read.record) : null;
|
|
241
241
|
const nextSlug = nextSlugFor({ stateRoot, taskId, slug, exec });
|
|
242
|
-
const result = (code
|
|
242
|
+
const result = (code , detail = null) => ({
|
|
243
243
|
ok: code === 'ready',
|
|
244
244
|
code,
|
|
245
245
|
record: read.record,
|
|
@@ -274,7 +274,7 @@ function checkReadiness({ stateRoot, taskId, slug = null, template, now = new Da
|
|
|
274
274
|
* @returns {Object} `{applies: false, reason}` or `{applies: true, runTaskId, stage, template, ok, code,
|
|
275
275
|
* record, message}`
|
|
276
276
|
*/
|
|
277
|
-
function verifyReadinessCheck({ stateRoot, taskId, slug = null, now = new Date(), exec }
|
|
277
|
+
function verifyReadinessCheck({ stateRoot, taskId, slug = null, now = new Date(), exec } ) {
|
|
278
278
|
let found = findRunFile({ stateRoot, taskId, slug, exec });
|
|
279
279
|
if (found.status === 'missing' && slug && slug !== taskId) found = findRunFile({ stateRoot, taskId: slug, slug, exec });
|
|
280
280
|
if (found.status === 'ambiguous') {
|
|
@@ -286,10 +286,10 @@ function verifyReadinessCheck({ stateRoot, taskId, slug = null, now = new Date()
|
|
|
286
286
|
if (found.status === 'missing') return { applies: false, reason: 'no-run' };
|
|
287
287
|
const run = readTaskState(fs, found.file);
|
|
288
288
|
if (run.state === 'COMPLETED' || run.state === 'REJECTED') return { applies: false, reason: 'run-finished' };
|
|
289
|
-
const stageNodes = (run.program || []).filter((n
|
|
289
|
+
const stageNodes = (run.program || []).filter((n ) => n.type === 'stage');
|
|
290
290
|
const stage = gatedStage(stageNodes);
|
|
291
291
|
if (!stage) return { applies: false, reason: 'no-gated-stage' };
|
|
292
|
-
const node = stageNodes.find((n
|
|
292
|
+
const node = stageNodes.find((n ) => n.id === stage.id);
|
|
293
293
|
if (node.status === 'completed' || node.status === 'skipped') return { applies: false, reason: 'gated-stage-done' };
|
|
294
294
|
|
|
295
295
|
const template = stage.readinessTemplate;
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
*/
|
|
46
46
|
|
|
47
47
|
const os = require('node:os');
|
|
48
|
-
const { readLock } = require('../../state/lockfile.
|
|
48
|
+
const { readLock } = require('../../state/lockfile.js');
|
|
49
49
|
|
|
50
50
|
/** The scopes a report is assembled from. Mirrors `readScopes`'s pair; a finding naming anything
|
|
51
51
|
* else is a caller bug and is reported as such rather than silently counted either way. */
|
|
@@ -111,9 +111,9 @@ const REPAIR_REASONS = Object.freeze({
|
|
|
111
111
|
|
|
112
112
|
/** The harnesses a lock document pins, and how it pins them. Mirrors `reconcile`'s own reading:
|
|
113
113
|
* a missing lock and a lock with an empty `targets` array are equally unable to repair anything. */
|
|
114
|
-
function scopePin(scope
|
|
114
|
+
function scopePin(scope , lock ) {
|
|
115
115
|
const targets = Array.isArray(lock?.targets)
|
|
116
|
-
? lock.targets.map((entry
|
|
116
|
+
? lock.targets.map((entry ) => entry?.harness).filter((harness ) => typeof harness === 'string' && harness !== '')
|
|
117
117
|
: [];
|
|
118
118
|
const state = !lock ? 'absent' : (targets.length === 0 ? 'empty' : 'present');
|
|
119
119
|
return { scope, lock: state, pinned: state === 'present', targets, findings: 0, repairable: 0 };
|
|
@@ -122,7 +122,7 @@ function scopePin(scope: any, lock: any): any {
|
|
|
122
122
|
/** Decide one finding against the pins. Repair-path reach is tested before lock reach: a finding
|
|
123
123
|
* no command performs is unreachable for a more fundamental reason than a narrow pin, and naming
|
|
124
124
|
* the pin would imply widening it is the fix. */
|
|
125
|
-
function classify(finding
|
|
125
|
+
function classify(finding , pins ) {
|
|
126
126
|
const pin = pins[finding?.scope];
|
|
127
127
|
if (!pin) return { repairable: false, reason: REPAIR_REASONS.UNKNOWN_SCOPE };
|
|
128
128
|
const declared = finding.repairPath;
|
|
@@ -139,7 +139,7 @@ function classify(finding: any, pins: any): any {
|
|
|
139
139
|
|
|
140
140
|
/** One sentence a reader meets before the detail, so the gap cannot be missed by someone who does
|
|
141
141
|
* not read the counts. States what is repairable and by what; never suggests a way to widen it. */
|
|
142
|
-
function summarize(total
|
|
142
|
+
function summarize(total , repairable ) {
|
|
143
143
|
if (total === 0) return 'No findings — repair coverage does not arise.';
|
|
144
144
|
if (repairable === total) {
|
|
145
145
|
return `All ${total} finding(s) are repairable by \`doflow reconcile\`.`;
|
|
@@ -164,7 +164,7 @@ function summarize(total: number, repairable: number): string {
|
|
|
164
164
|
* @throws when a lock file exists but cannot be parsed or validated. Propagated deliberately:
|
|
165
165
|
* `reconcile` would fail on the same document, so guessing its reach would be fiction.
|
|
166
166
|
*/
|
|
167
|
-
function readScopeLocks({ projectRoot, homeDir = os.homedir() }
|
|
167
|
+
function readScopeLocks({ projectRoot, homeDir = os.homedir() } , { fsImpl } = {}) {
|
|
168
168
|
return {
|
|
169
169
|
global: readLock({ scope: 'global', homeDir }, { fsImpl }),
|
|
170
170
|
project: readLock({ scope: 'project', projectRoot }, { fsImpl }),
|
|
@@ -180,12 +180,12 @@ function readScopeLocks({ projectRoot, homeDir = os.homedir() }: any, { fsImpl }
|
|
|
180
180
|
* as `readScopeLocks` returns them. A missing or `null` entry is an unpinned scope.
|
|
181
181
|
* @returns {RepairCoverage}
|
|
182
182
|
*/
|
|
183
|
-
function repairCoverage({ findings = [], locks = {} }
|
|
183
|
+
function repairCoverage({ findings = [], locks = {} } = {}) {
|
|
184
184
|
const rows = Array.isArray(findings) ? findings : [];
|
|
185
185
|
const pins = Object.fromEntries(SCOPES.map((scope) => [scope, scopePin(scope, locks?.[scope] ?? null)]));
|
|
186
186
|
|
|
187
|
-
const reasons
|
|
188
|
-
const details = rows.map((finding
|
|
187
|
+
const reasons = {};
|
|
188
|
+
const details = rows.map((finding ) => {
|
|
189
189
|
const { repairable, reason } = classify(finding, pins);
|
|
190
190
|
const pin = pins[finding?.scope];
|
|
191
191
|
if (pin) {
|
|
@@ -202,7 +202,7 @@ function repairCoverage({ findings = [], locks = {} }: any = {}): any {
|
|
|
202
202
|
};
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
-
const repairable = details.filter((row
|
|
205
|
+
const repairable = details.filter((row ) => row.repairable).length;
|
|
206
206
|
return {
|
|
207
207
|
findings: details.length,
|
|
208
208
|
repairable,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Two recorded resources describe the same logical asset when two parts agree: the harness, and the
|
|
9
9
|
* ownership identity the adapter recorded for that resource. That pair is not a new key — it is the
|
|
10
|
-
* stored ownership key (`src/state/index.
|
|
10
|
+
* stored ownership key (`src/state/index.js#ownershipKey`, five components) with its two
|
|
11
11
|
* scope-bearing components dropped: the `scope`, which names itself, and the absolute `target`,
|
|
12
12
|
* which differs between a global and a project copy of the same asset by construction. So this is a
|
|
13
13
|
* projection of a key the ledger already holds, not a value reconstructed from a path; nothing is
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
* user can hand-edit. `null` is a signal to the caller to report the row as unresolved, never a
|
|
54
54
|
* value to join or compare against another `null`.
|
|
55
55
|
*/
|
|
56
|
-
function deriveAssetIdentity(resource
|
|
56
|
+
function deriveAssetIdentity(resource ) {
|
|
57
57
|
if (!resource || typeof resource !== 'object') return null;
|
|
58
58
|
const { harness, ownershipIdentity } = resource;
|
|
59
59
|
if (typeof harness !== 'string' || harness === '') return null;
|
|
@@ -59,13 +59,13 @@
|
|
|
59
59
|
const fs = require('node:fs');
|
|
60
60
|
const path = require('node:path');
|
|
61
61
|
const crypto = require('node:crypto');
|
|
62
|
-
const { harnessFor, loadRegistry } = require('../../registry/index.
|
|
63
|
-
const { readScopes } = require('./read-scopes.
|
|
64
|
-
const { deriveAssetIdentity } = require('./identity.
|
|
65
|
-
const { resolveShadowVerdict } = require('./precedence.
|
|
66
|
-
const { inspectSiblings } = require('./siblings.
|
|
67
|
-
const { repairCoverage, readScopeLocks } = require('./coverage.
|
|
68
|
-
const { finishRuntime, usageError } = require('../cli-result.
|
|
62
|
+
const { harnessFor, loadRegistry } = require('../../registry/index.js');
|
|
63
|
+
const { readScopes } = require('./read-scopes.js');
|
|
64
|
+
const { deriveAssetIdentity } = require('./identity.js');
|
|
65
|
+
const { resolveShadowVerdict } = require('./precedence.js');
|
|
66
|
+
const { inspectSiblings } = require('./siblings.js');
|
|
67
|
+
const { repairCoverage, readScopeLocks } = require('./coverage.js');
|
|
68
|
+
const { finishRuntime, usageError } = require('../cli-result.js');
|
|
69
69
|
|
|
70
70
|
/** The three currency values of IC-002, and never a fourth. The third is not a weaker second: a
|
|
71
71
|
* copy nothing in this scope's plan spoke about has not "diverged", it cannot be judged at all.
|
|
@@ -110,14 +110,14 @@ const RECONCILE = 'reconcile';
|
|
|
110
110
|
const MANUAL = 'manual';
|
|
111
111
|
|
|
112
112
|
/** Report vocabulary -> registry vocabulary (`scopePrecedence.order` names the global scope `user`). */
|
|
113
|
-
function toRegistryScope(scope
|
|
113
|
+
function toRegistryScope(scope ) { return scope === 'global' ? 'user' : scope; }
|
|
114
114
|
/** Registry vocabulary -> report vocabulary. The inverse of the above, applied to every scope name
|
|
115
115
|
* `precedence.js` hands back, so a verdict never names a scope the rest of the report does not. */
|
|
116
|
-
function toReportScope(scope
|
|
116
|
+
function toReportScope(scope ) { return scope === 'user' ? 'global' : scope; }
|
|
117
117
|
|
|
118
118
|
/** The four fields that identify one recorded resource, and the same four an adapter change carries.
|
|
119
119
|
* Not `ownershipKey` from src/state: that one includes the scope, and a plan change has none. */
|
|
120
|
-
function recordKey(record
|
|
120
|
+
function recordKey(record ) {
|
|
121
121
|
return [record?.harness, record?.assetId, record?.target, record?.ownershipIdentity].join('\u0000');
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -141,7 +141,7 @@ function recordKey(record: any): string {
|
|
|
141
141
|
* component boundaries here. A prerequisite is deliberately *not* disqualifying — it says the change
|
|
142
142
|
* cannot yet be applied, not that the plan could not determine it.
|
|
143
143
|
*/
|
|
144
|
-
function planBasis(snapshot
|
|
144
|
+
function planBasis(snapshot ) {
|
|
145
145
|
const changes = new Map();
|
|
146
146
|
for (const change of snapshot?.plan?.changes ?? []) {
|
|
147
147
|
if (change && typeof change === 'object') changes.set(recordKey(change), change);
|
|
@@ -181,7 +181,7 @@ function planBasis(snapshot: any): any {
|
|
|
181
181
|
* @returns {boolean} false whenever the destination cannot be read, or the change names no
|
|
182
182
|
* fingerprint — an absent or unreadable file is no evidence that its content is current.
|
|
183
183
|
*/
|
|
184
|
-
function contentAlreadyCurrent(change
|
|
184
|
+
function contentAlreadyCurrent(change , fsImpl ) {
|
|
185
185
|
const intended = change?.fingerprint;
|
|
186
186
|
if (typeof intended !== 'string' || intended === '') return false;
|
|
187
187
|
let observed;
|
|
@@ -231,7 +231,7 @@ function contentAlreadyCurrent(change: any, fsImpl: any): boolean {
|
|
|
231
231
|
* `null` for the other two values — a copy that was judged needs no basis, and a diverged one's
|
|
232
232
|
* remedy is the restore `remedyFor` already states.
|
|
233
233
|
*/
|
|
234
|
-
function currencyOf(resource
|
|
234
|
+
function currencyOf(resource , basis , fsImpl = fs) {
|
|
235
235
|
const recorded = resource?.fingerprint;
|
|
236
236
|
if (typeof recorded !== 'string' || recorded === '') {
|
|
237
237
|
return {
|
|
@@ -294,13 +294,13 @@ function currencyOf(resource: any, basis: any, fsImpl: any = fs): any {
|
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
/** The literal command that would converge one scope. Text only — this verb never runs it. */
|
|
297
|
-
function reconcileCommand(scope
|
|
297
|
+
function reconcileCommand(scope , projectRoot ) {
|
|
298
298
|
return scope === 'global' ? 'doflow reconcile -g' : `doflow reconcile ${projectRoot}`;
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
/** One copy of a logical asset, as IC-002 requires: where it is, and how current it is — plus, where
|
|
302
302
|
* it could not be judged at all, what would give a later run a basis for judging it. */
|
|
303
|
-
function copyEntry(resource
|
|
303
|
+
function copyEntry(resource , scope , basis , fsImpl ) {
|
|
304
304
|
const currency = currencyOf(resource, basis, fsImpl);
|
|
305
305
|
return {
|
|
306
306
|
scope,
|
|
@@ -321,9 +321,9 @@ function copyEntry(resource: any, scope: any, basis: any, fsImpl: any): any {
|
|
|
321
321
|
* @returns {{groups: Map<string, Object>, unidentified: Array<Object>}} `unidentified` holds one
|
|
322
322
|
* row per resource whose identity could not be derived — never grouped, since `null` is not a key.
|
|
323
323
|
*/
|
|
324
|
-
function groupByIdentity(snapshots
|
|
324
|
+
function groupByIdentity(snapshots , fsImpl ) {
|
|
325
325
|
const groups = new Map();
|
|
326
|
-
const unidentified
|
|
326
|
+
const unidentified = [];
|
|
327
327
|
for (const snapshot of snapshots) {
|
|
328
328
|
const basis = planBasis(snapshot);
|
|
329
329
|
for (const resource of snapshot.resources ?? []) {
|
|
@@ -351,15 +351,15 @@ function groupByIdentity(snapshots: any, fsImpl: any): any {
|
|
|
351
351
|
/** The harness's recorded precedence block, or `null` when it declares none — the fifth IC-003
|
|
352
352
|
* state (the question has not been asked), which `precedence.js` must see as an absence rather than
|
|
353
353
|
* as an empty object it could misread as a block with no mode. */
|
|
354
|
-
function precedenceFor(registry
|
|
354
|
+
function precedenceFor(registry , harness ) {
|
|
355
355
|
try { return harnessFor(registry, harness).scopePrecedence ?? null; }
|
|
356
356
|
catch { return null; }
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
/** Every unmanaged entry sharing a directory with one of this asset's copies, attributed to the
|
|
360
360
|
* scope whose copy put it in view. Never carries a currency (data model: `UNMANAGED_CANDIDATE`). */
|
|
361
|
-
function unmanagedFor(copies
|
|
362
|
-
const found
|
|
361
|
+
function unmanagedFor(copies , candidatesByTarget ) {
|
|
362
|
+
const found = [];
|
|
363
363
|
for (const copy of copies) {
|
|
364
364
|
for (const candidate of candidatesByTarget[copy.location] ?? []) {
|
|
365
365
|
found.push({ ...candidate, scope: copy.scope });
|
|
@@ -371,8 +371,8 @@ function unmanagedFor(copies: any, candidatesByTarget: any): any[] {
|
|
|
371
371
|
/** The literal actions that would resolve this asset's findings, in the order a reader should take
|
|
372
372
|
* them. One object per asset (IC-002's "the remedy"), holding every step rather than the first, so
|
|
373
373
|
* an asset that is both diverged and shadowed does not have half its remedy silently dropped. */
|
|
374
|
-
function remedyFor({ copies, verdict, unmanaged, projectRoot }
|
|
375
|
-
const actions
|
|
374
|
+
function remedyFor({ copies, verdict, unmanaged, projectRoot } ) {
|
|
375
|
+
const actions = [];
|
|
376
376
|
for (const copy of copies) {
|
|
377
377
|
if (copy.currency === CURRENCY.DIVERGED) {
|
|
378
378
|
actions.push({
|
|
@@ -393,7 +393,7 @@ function remedyFor({ copies, verdict, unmanaged, projectRoot }: any): any {
|
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
395
|
if (verdict?.shadowed && verdict.winner) {
|
|
396
|
-
const losers = copies.filter((copy
|
|
396
|
+
const losers = copies.filter((copy ) => copy.scope !== verdict.winner).map((copy ) => copy.location);
|
|
397
397
|
actions.push({
|
|
398
398
|
finding: FINDING_KINDS.SHADOW,
|
|
399
399
|
action: `edit the copy the harness actually loads — the ${verdict.winner} one — or delete the `
|
|
@@ -404,7 +404,7 @@ function remedyFor({ copies, verdict, unmanaged, projectRoot }: any): any {
|
|
|
404
404
|
if (verdict?.shadowed && !verdict.winner) {
|
|
405
405
|
actions.push({
|
|
406
406
|
finding: FINDING_KINDS.WITHHELD_VERDICT,
|
|
407
|
-
action: `keep exactly one copy — ${copies.map((copy
|
|
407
|
+
action: `keep exactly one copy — ${copies.map((copy ) => copy.location).join(' or ')} — because `
|
|
408
408
|
+ `${verdict.withheldReason}, so which one takes effect cannot be stated`,
|
|
409
409
|
command: null,
|
|
410
410
|
});
|
|
@@ -449,8 +449,8 @@ function remedyFor({ copies, verdict, unmanaged, projectRoot }: any): any {
|
|
|
449
449
|
* finding about it. The harness is in the key because a directory two harnesses both load
|
|
450
450
|
* wholesale makes the same file operative twice, which is two facts.
|
|
451
451
|
*/
|
|
452
|
-
function findingsFor({ identity, harness, copies, verdict, unmanaged }
|
|
453
|
-
const findings
|
|
452
|
+
function findingsFor({ identity, harness, copies, verdict, unmanaged } , seenUnmanaged ) {
|
|
453
|
+
const findings = [];
|
|
454
454
|
for (const copy of copies) {
|
|
455
455
|
if (copy.currency === CURRENCY.DIVERGED) {
|
|
456
456
|
findings.push({ id: identity, kind: FINDING_KINDS.DIVERGENCE, harness, scope: copy.scope, repairPath: RECONCILE });
|
|
@@ -484,7 +484,7 @@ function findingsFor({ identity, harness, copies, verdict, unmanaged }: any, see
|
|
|
484
484
|
* A restriction to named harnesses is named in the sentence, because the sentence is where a reader
|
|
485
485
|
* decides whether to act. "nothing requires attention" read off a run restricted to one harness — or
|
|
486
486
|
* to a harness that is not installed — would otherwise be taken for a statement about the install. */
|
|
487
|
-
function summarize({ findings, assets, unidentified, targets }
|
|
487
|
+
function summarize({ findings, assets, unidentified, targets } ) {
|
|
488
488
|
const counts = new Map();
|
|
489
489
|
for (const finding of findings) counts.set(finding.kind, (counts.get(finding.kind) ?? 0) + 1);
|
|
490
490
|
const subject = `${assets.length} logical asset(s)${targets ? ` recorded for ${targets.join(', ')}` : ''}`;
|
|
@@ -531,7 +531,7 @@ function summarize({ findings, assets, unidentified, targets }: any): string {
|
|
|
531
531
|
* exists but cannot be read is the same class as a ledger that cannot: the report would have to
|
|
532
532
|
* invent the selection the plan is computed from.
|
|
533
533
|
*/
|
|
534
|
-
function buildInventoryReport({ repoRoot, projectRoot, targets, registry, locks, fsImpl }
|
|
534
|
+
function buildInventoryReport({ repoRoot, projectRoot, targets, registry, locks, fsImpl } = {}) {
|
|
535
535
|
const loaded = registry ?? loadRegistry({ repoRoot });
|
|
536
536
|
const requested = Array.isArray(targets) && targets.length > 0 ? targets : undefined;
|
|
537
537
|
// Validated before anything is read, so an unknown harness fails as the argument error it is
|
|
@@ -550,12 +550,12 @@ function buildInventoryReport({ repoRoot, projectRoot, targets, registry, locks,
|
|
|
550
550
|
|
|
551
551
|
const { groups, unidentified } = groupByIdentity(scopes, fsImpl ?? fs);
|
|
552
552
|
const siblings = inspectSiblings({
|
|
553
|
-
resources: scopes.flatMap((snapshot
|
|
553
|
+
resources: scopes.flatMap((snapshot ) => snapshot.resources ?? []),
|
|
554
554
|
...(fsImpl ? { fsImpl } : {}),
|
|
555
555
|
});
|
|
556
556
|
|
|
557
|
-
const assets
|
|
558
|
-
const findings
|
|
557
|
+
const assets = [];
|
|
558
|
+
const findings = [];
|
|
559
559
|
// One finding per unmanaged file across the whole report, not per asset that lists it (FR-007;
|
|
560
560
|
// see the header's note 3). Assets are walked in identity order, so which entry carries the
|
|
561
561
|
// finding is deterministic.
|
|
@@ -563,7 +563,7 @@ function buildInventoryReport({ repoRoot, projectRoot, targets, registry, locks,
|
|
|
563
563
|
for (const identity of [...groups.keys()].sort()) {
|
|
564
564
|
const { harness, assetId, copies } = groups.get(identity);
|
|
565
565
|
// Both crossings of the vocabulary seam, in one place: in, then straight back out.
|
|
566
|
-
const raw = resolveShadowVerdict(copies.map((copy
|
|
566
|
+
const raw = resolveShadowVerdict(copies.map((copy ) => toRegistryScope(copy.scope)), precedenceFor(loaded, harness));
|
|
567
567
|
const verdict = raw && {
|
|
568
568
|
presentAt: raw.presentAt.map(toReportScope),
|
|
569
569
|
shadowed: raw.shadowed,
|
|
@@ -603,7 +603,7 @@ function buildInventoryReport({ repoRoot, projectRoot, targets, registry, locks,
|
|
|
603
603
|
status: findings.length === 0 ? 'CLEAN' : 'FINDINGS',
|
|
604
604
|
exitCode: findings.length === 0 ? 0 : 1,
|
|
605
605
|
summary: summarize({ findings, assets, unidentified, targets: requested }),
|
|
606
|
-
scopes: scopes.map((snapshot
|
|
606
|
+
scopes: scopes.map((snapshot ) => ({
|
|
607
607
|
scope: snapshot.scope,
|
|
608
608
|
root: snapshot.scopeRoot,
|
|
609
609
|
present: snapshot.recorded,
|
|
@@ -618,7 +618,7 @@ function buildInventoryReport({ repoRoot, projectRoot, targets, registry, locks,
|
|
|
618
618
|
|
|
619
619
|
/** Human rendering. The same facts as `--json`, in the same order, so neither shape is the one that
|
|
620
620
|
* tells the truth. */
|
|
621
|
-
function printReport(report
|
|
621
|
+
function printReport(report ) {
|
|
622
622
|
console.log('\nDoFlow cross-scope inventory:');
|
|
623
623
|
console.log('═'.repeat(78));
|
|
624
624
|
console.log(report.summary);
|
|
@@ -679,7 +679,7 @@ function printReport(report: any): void {
|
|
|
679
679
|
* question from the one the caller asked.
|
|
680
680
|
* @returns {number} the exit status, already set on the process
|
|
681
681
|
*/
|
|
682
|
-
function handleInventoryCommand({ repoRoot, projectRoot, targets, json = false, global: globalScope = false }
|
|
682
|
+
function handleInventoryCommand({ repoRoot, projectRoot, targets, json = false, global: globalScope = false } = {}) {
|
|
683
683
|
if (globalScope) {
|
|
684
684
|
return usageError('inventory', '-g/--global is not accepted: this verb reads both scopes in one '
|
|
685
685
|
+ 'invocation, which is its entire purpose. Re-run without the flag.', json);
|
|
@@ -691,7 +691,7 @@ function handleInventoryCommand({ repoRoot, projectRoot, targets, json = false,
|
|
|
691
691
|
} catch (error) {
|
|
692
692
|
// IC-001 assigns an unreadable registry, an unparseable ledger and an undeclared harness alike
|
|
693
693
|
// to exit 2. A scope with nothing recorded never reaches here — `readScopes` returns it empty.
|
|
694
|
-
return usageError('inventory', (error
|
|
694
|
+
return usageError('inventory', (error ).message, json, error);
|
|
695
695
|
}
|
|
696
696
|
|
|
697
697
|
if (json) console.log(JSON.stringify(report, null, 2));
|