@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
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
|
|
18
18
|
const nodeFs = require('node:fs');
|
|
19
19
|
const path = require('node:path');
|
|
20
|
-
const { acquireLock } = require('./task-state.
|
|
21
|
-
const { resolveActiveFeature } = require('./feature-resolve.
|
|
22
|
-
const { compactHistory } = require('./history-compactor.
|
|
23
|
-
const { finishRuntime, usageError } = require('./cli-result.
|
|
20
|
+
const { acquireLock } = require('./task-state.js');
|
|
21
|
+
const { resolveActiveFeature } = require('./feature-resolve.js');
|
|
22
|
+
const { compactHistory } = require('./history-compactor.js');
|
|
23
|
+
const { finishRuntime, usageError } = require('./cli-result.js');
|
|
24
24
|
|
|
25
25
|
const REGISTER_VERSION = 1;
|
|
26
26
|
const TOPIC_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
@@ -30,7 +30,7 @@ const REF_RE = /^[A-Za-z]+-?[0-9]+$/;
|
|
|
30
30
|
const DEC_RE = /^DEC-[0-9]{3,}$/;
|
|
31
31
|
const STAGES = ['discovery', 'design', 'planning', 'implementation', 'verification', 'review'];
|
|
32
32
|
/** Channel -> who decided. The caller names the channel; `decidedBy` follows from it. */
|
|
33
|
-
const CHANNEL_DECIDED_BY
|
|
33
|
+
const CHANNEL_DECIDED_BY = { question: 'user', gate: 'user', prompt: 'user', default: 'agent', resolution: 'agent' };
|
|
34
34
|
const ITEM_KEYS = ['topic', 'statement', 'channel', 'stage', 'rationale', 'supersedes', 'refs', 'source'];
|
|
35
35
|
const REQUIRED_KEYS = ['topic', 'statement', 'channel', 'stage', 'rationale'];
|
|
36
36
|
|
|
@@ -44,9 +44,9 @@ class DecisionUsageError extends Error {}
|
|
|
44
44
|
|
|
45
45
|
/** A finding the caller must act on (exit 1). */
|
|
46
46
|
class DecisionFinding extends Error {
|
|
47
|
-
finding
|
|
47
|
+
finding ;
|
|
48
48
|
|
|
49
|
-
constructor(finding
|
|
49
|
+
constructor(finding , message ) {
|
|
50
50
|
super(message);
|
|
51
51
|
this.finding = finding;
|
|
52
52
|
}
|
|
@@ -54,19 +54,19 @@ class DecisionFinding extends Error {
|
|
|
54
54
|
|
|
55
55
|
// ── paths ──────────────────────────────────────────────────────────────────────────────────────
|
|
56
56
|
|
|
57
|
-
function registerFile(featureDir
|
|
58
|
-
function liveViewFile(featureDir
|
|
59
|
-
function archiveFile(featureDir
|
|
57
|
+
function registerFile(featureDir ) { return path.join(featureDir, 'decisions', 'register.json'); }
|
|
58
|
+
function liveViewFile(featureDir ) { return path.join(featureDir, 'decisions.md'); }
|
|
59
|
+
function archiveFile(featureDir ) { return path.join(featureDir, 'decisions', 'archive.md'); }
|
|
60
60
|
|
|
61
61
|
// ── register io ────────────────────────────────────────────────────────────────────────────────
|
|
62
62
|
|
|
63
|
-
function readRegister(featureDir
|
|
63
|
+
function readRegister(featureDir , fsImpl = nodeFs) {
|
|
64
64
|
const file = registerFile(featureDir);
|
|
65
65
|
if (!fsImpl.existsSync(file)) return null;
|
|
66
|
-
let data
|
|
66
|
+
let data ;
|
|
67
67
|
try {
|
|
68
68
|
data = JSON.parse(fsImpl.readFileSync(file, 'utf8'));
|
|
69
|
-
} catch (error
|
|
69
|
+
} catch (error ) {
|
|
70
70
|
throw new DecisionUsageError(`${file} is not valid JSON (${error.message}). The register is written atomically, so a torn file means outside interference; restore it from version control.`);
|
|
71
71
|
}
|
|
72
72
|
if (!data || data.version !== REGISTER_VERSION || !Array.isArray(data.decisions) || !Number.isInteger(data.nextId)) {
|
|
@@ -75,62 +75,62 @@ function readRegister(featureDir: string, fsImpl: any = nodeFs): any {
|
|
|
75
75
|
return data;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function writeAtomic(fsImpl
|
|
78
|
+
function writeAtomic(fsImpl , file , content ) {
|
|
79
79
|
fsImpl.mkdirSync(path.dirname(file), { recursive: true });
|
|
80
80
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
81
81
|
try {
|
|
82
82
|
fsImpl.writeFileSync(tmp, content, 'utf8');
|
|
83
83
|
fsImpl.renameSync(tmp, file);
|
|
84
|
-
} catch (error
|
|
84
|
+
} catch (error ) {
|
|
85
85
|
try { fsImpl.rmSync(tmp, { force: true }); } catch { /* nothing to clean */ }
|
|
86
86
|
throw error;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/** Writes only when the content differs, so re-rendering an unchanged view is a no-op. */
|
|
91
|
-
function writeIfChanged(fsImpl
|
|
91
|
+
function writeIfChanged(fsImpl , file , content ) {
|
|
92
92
|
if (fsImpl.existsSync(file) && fsImpl.readFileSync(file, 'utf8') === content) return;
|
|
93
93
|
writeAtomic(fsImpl, file, content);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
function writeRegister(fsImpl
|
|
96
|
+
function writeRegister(fsImpl , featureDir , register ) {
|
|
97
97
|
writeAtomic(fsImpl, registerFile(featureDir), `${JSON.stringify(register, null, 2)}\n`);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
function renderViews(fsImpl
|
|
100
|
+
function renderViews(fsImpl , featureDir , register ) {
|
|
101
101
|
writeIfChanged(fsImpl, liveViewFile(featureDir), renderLiveView(register));
|
|
102
102
|
writeIfChanged(fsImpl, archiveFile(featureDir), renderArchive(register));
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// ── ordering and ids ───────────────────────────────────────────────────────────────────────────
|
|
106
106
|
|
|
107
|
-
function idNumber(id
|
|
108
|
-
function formatId(n
|
|
109
|
-
function byId(a
|
|
110
|
-
function byTopic(a
|
|
111
|
-
function liveOf(register
|
|
107
|
+
function idNumber(id ) { return Number(id.slice(4)); }
|
|
108
|
+
function formatId(n ) { return `DEC-${String(n).padStart(3, '0')}`; }
|
|
109
|
+
function byId(a , b ) { return idNumber(a.id) - idNumber(b.id); }
|
|
110
|
+
function byTopic(a , b ) { return a.topic < b.topic ? -1 : a.topic > b.topic ? 1 : idNumber(a.id) - idNumber(b.id); }
|
|
111
|
+
function liveOf(register ) { return register.decisions.filter((d ) => d.status === 'live'); }
|
|
112
112
|
|
|
113
113
|
// ── renderers ──────────────────────────────────────────────────────────────────────────────────
|
|
114
114
|
|
|
115
115
|
/** `\` is escaped first, so a statement ending in one cannot escape the pipe that closes the cell
|
|
116
116
|
* (IC-007); `|` would end the cell; `<` could open an HTML comment that hides every later row. */
|
|
117
|
-
function cell(text
|
|
117
|
+
function cell(text ) { return String(text).replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/</g, '\\<'); }
|
|
118
118
|
|
|
119
119
|
/** Archive text is read by people and agents, never as HTML: `<` is escaped so `<!--` cannot hide later entries. */
|
|
120
|
-
function plain(text
|
|
120
|
+
function plain(text ) { return String(text).replace(/</g, '\\<'); }
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* A one-line rationale reads inline. A longer one goes in a blockquote under the bullet, every line
|
|
124
124
|
* prefixed, so no line of it can start a heading or pose as one of the entry's own bullets.
|
|
125
125
|
*/
|
|
126
|
-
function rationaleLines(rationale
|
|
126
|
+
function rationaleLines(rationale ) {
|
|
127
127
|
const parts = plain(rationale).split(/\r\n|[\n\r\u2028\u2029\u0085]/);
|
|
128
128
|
if (parts.length === 1) return [`- **Rationale:** ${parts[0]}`];
|
|
129
|
-
return ['- **Rationale:**', ...parts.map((l
|
|
129
|
+
return ['- **Rationale:**', ...parts.map((l ) => (l === '' ? ' >' : ` > ${l}`))];
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/** IC-007. Size depends on the number of live topics, never on how many decisions were made. */
|
|
133
|
-
function renderLiveView(register
|
|
133
|
+
function renderLiveView(register ) {
|
|
134
134
|
const live = liveOf(register).sort(byTopic);
|
|
135
135
|
const out = [
|
|
136
136
|
`# Decisions: ${register.slug}`,
|
|
@@ -150,7 +150,7 @@ function renderLiveView(register: any): string {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
/** IC-008. One entry per decision, by id; superseded entries stay with their rationale. */
|
|
153
|
-
function renderArchive(register
|
|
153
|
+
function renderArchive(register ) {
|
|
154
154
|
const out = [`# Decision archive: ${register.slug}`, '', `> ${GENERATED_NOTE}`];
|
|
155
155
|
for (const d of [...register.decisions].sort(byId)) {
|
|
156
156
|
out.push('', `### ${d.id}: ${d.topic}`, '');
|
|
@@ -167,13 +167,13 @@ function renderArchive(register: any): string {
|
|
|
167
167
|
|
|
168
168
|
// ── item validation ────────────────────────────────────────────────────────────────────────────
|
|
169
169
|
|
|
170
|
-
function toList(value
|
|
170
|
+
function toList(value , what , label ) {
|
|
171
171
|
if (value === undefined || value === null || value === '') return [];
|
|
172
|
-
let list
|
|
172
|
+
let list ;
|
|
173
173
|
if (Array.isArray(value)) list = value;
|
|
174
174
|
else if (typeof value === 'string') list = value.split(',');
|
|
175
175
|
else throw new DecisionUsageError(`${label}: ${what} must be an array of strings`);
|
|
176
|
-
const out
|
|
176
|
+
const out = [];
|
|
177
177
|
for (const entry of list) {
|
|
178
178
|
if (typeof entry !== 'string') throw new DecisionUsageError(`${label}: ${what} must be an array of strings`);
|
|
179
179
|
const trimmed = entry.trim();
|
|
@@ -183,11 +183,11 @@ function toList(value: any, what: string, label: string): string[] {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
/** Validates one add item against IC-003's field rules. Throws DecisionUsageError. */
|
|
186
|
-
function normalizeItem(raw
|
|
186
|
+
function normalizeItem(raw , label ) {
|
|
187
187
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new DecisionUsageError(`${label}: must be an object`);
|
|
188
|
-
const extra = Object.keys(raw).filter((k
|
|
188
|
+
const extra = Object.keys(raw).filter((k ) => !ITEM_KEYS.includes(k));
|
|
189
189
|
if (extra.length) {
|
|
190
|
-
throw new DecisionUsageError(`${label}: unknown key${extra.length > 1 ? 's' : ''} ${extra.map((k
|
|
190
|
+
throw new DecisionUsageError(`${label}: unknown key${extra.length > 1 ? 's' : ''} ${extra.map((k ) => `'${k}'`).join(', ')}. Allowed: ${ITEM_KEYS.join(', ')} (decidedBy is set by the runtime from the channel)`);
|
|
191
191
|
}
|
|
192
192
|
for (const key of REQUIRED_KEYS) {
|
|
193
193
|
if (typeof raw[key] !== 'string' || raw[key].trim() === '') throw new DecisionUsageError(`${label}: '${key}' is required`);
|
|
@@ -226,9 +226,9 @@ function normalizeItem(raw: any, label: string): any {
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
/** Normalizes the batch (`--batch` contents) or the single item, refusing everything on one fault. */
|
|
229
|
-
function normalizeItems(items
|
|
229
|
+
function normalizeItems(items ) {
|
|
230
230
|
if (!Array.isArray(items)) throw new DecisionUsageError('a batch must be a JSON array of decision objects');
|
|
231
|
-
const normalized = items.map((raw
|
|
231
|
+
const normalized = items.map((raw , i ) => normalizeItem(raw, `item ${i + 1}`));
|
|
232
232
|
const seen = new Set();
|
|
233
233
|
for (const item of normalized) {
|
|
234
234
|
if (seen.has(item.topic)) throw new DecisionUsageError(`topic '${item.topic}' appears more than once in the batch; a topic takes one decision per batch`);
|
|
@@ -239,15 +239,15 @@ function normalizeItems(items: any): any[] {
|
|
|
239
239
|
|
|
240
240
|
// ── actions ────────────────────────────────────────────────────────────────────────────────────
|
|
241
241
|
|
|
242
|
-
function summary({ action, slug, featureDir }
|
|
242
|
+
function summary({ action, slug, featureDir } ) { return { action, slug, featureDir }; }
|
|
243
243
|
|
|
244
244
|
const FOLDER_ARTIFACTS = ['intention/requirement.md', 'requirement.md', 'design/design.md', 'design.md', 'plan.md'];
|
|
245
245
|
|
|
246
246
|
/** IC-002. */
|
|
247
|
-
function initRegister({ featureDir, slug, fsImpl = nodeFs }
|
|
247
|
+
function initRegister({ featureDir, slug, fsImpl = nodeFs } ) {
|
|
248
248
|
const base = summary({ action: 'init', slug, featureDir });
|
|
249
249
|
if (fsImpl.existsSync(registerFile(featureDir))) return { ...base, created: false };
|
|
250
|
-
const present = FOLDER_ARTIFACTS.filter((rel
|
|
250
|
+
const present = FOLDER_ARTIFACTS.filter((rel ) => fsImpl.existsSync(path.join(featureDir, rel)));
|
|
251
251
|
if (present.length) {
|
|
252
252
|
return {
|
|
253
253
|
...base,
|
|
@@ -273,7 +273,7 @@ function initRegister({ featureDir, slug, fsImpl = nodeFs }: any): any {
|
|
|
273
273
|
* IC-003. All-or-nothing: the whole batch is validated and simulated against the register first,
|
|
274
274
|
* and nothing is written unless every item passes.
|
|
275
275
|
*/
|
|
276
|
-
function addDecisions({ featureDir, slug, items, now = new Date(), fsImpl = nodeFs }
|
|
276
|
+
function addDecisions({ featureDir, slug, items, now = new Date(), fsImpl = nodeFs } ) {
|
|
277
277
|
const normalized = normalizeItems(items);
|
|
278
278
|
const base = summary({ action: 'add', slug, featureDir });
|
|
279
279
|
if (!fsImpl.existsSync(registerFile(featureDir))) {
|
|
@@ -285,24 +285,24 @@ function addDecisions({ featureDir, slug, items, now = new Date(), fsImpl = node
|
|
|
285
285
|
try {
|
|
286
286
|
const register = readRegister(featureDir, fsImpl);
|
|
287
287
|
const iso = now.toISOString();
|
|
288
|
-
const added
|
|
289
|
-
const superseded
|
|
290
|
-
let conflict
|
|
288
|
+
const added = [];
|
|
289
|
+
const superseded = [];
|
|
290
|
+
let conflict = null;
|
|
291
291
|
let nextId = register.nextId;
|
|
292
292
|
for (const item of normalized) {
|
|
293
293
|
for (const id of item.supersedes) {
|
|
294
|
-
const target = register.decisions.find((d
|
|
294
|
+
const target = register.decisions.find((d ) => d.id === id);
|
|
295
295
|
if (!target) throw new DecisionUsageError(`topic '${item.topic}': supersedes ${id}, which is not a decision in this register`);
|
|
296
296
|
if (target.status !== 'live') throw new DecisionUsageError(`topic '${item.topic}': supersedes ${id}, which is not live (it was superseded by ${target.supersededBy})`);
|
|
297
297
|
}
|
|
298
|
-
const unnamed = register.decisions.find((d
|
|
298
|
+
const unnamed = register.decisions.find((d ) => d.status === 'live' && d.topic === item.topic && !item.supersedes.includes(d.id));
|
|
299
299
|
if (unnamed && !conflict) {
|
|
300
300
|
conflict = { finding: 'topic-conflict', message: `topic '${item.topic}' already has live decision ${unnamed.id}; name it in supersedes to replace it. Nothing was written` };
|
|
301
301
|
}
|
|
302
302
|
const id = formatId(nextId);
|
|
303
303
|
nextId += 1;
|
|
304
304
|
for (const old of item.supersedes) {
|
|
305
|
-
const target = register.decisions.find((d
|
|
305
|
+
const target = register.decisions.find((d ) => d.id === old);
|
|
306
306
|
target.status = 'superseded';
|
|
307
307
|
target.supersededBy = id;
|
|
308
308
|
superseded.push({ id: old, by: id });
|
|
@@ -336,7 +336,7 @@ function addDecisions({ featureDir, slug, items, now = new Date(), fsImpl = node
|
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
/** IC-004. */
|
|
339
|
-
function listDecisions({ featureDir, slug, all = false, fsImpl = nodeFs }
|
|
339
|
+
function listDecisions({ featureDir, slug, all = false, fsImpl = nodeFs } ) {
|
|
340
340
|
const base = summary({ action: 'list', slug, featureDir });
|
|
341
341
|
const register = readRegister(featureDir, fsImpl);
|
|
342
342
|
if (!register) return { ...base, finding: 'no-register', message: `${featureDir} has no decisions/register.json` };
|
|
@@ -345,7 +345,7 @@ function listDecisions({ featureDir, slug, all = false, fsImpl = nodeFs }: any):
|
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
/** IC-005: compaction plus the view re-render, under the register lock `compactHistory` itself lacks. */
|
|
348
|
-
function compactDecisions({ featureDir, slug, repoRoot, paths, now = new Date(), fsImpl = nodeFs }
|
|
348
|
+
function compactDecisions({ featureDir, slug, repoRoot, paths, now = new Date(), fsImpl = nodeFs } ) {
|
|
349
349
|
const base = summary({ action: 'compact', slug, featureDir });
|
|
350
350
|
if (!fsImpl.existsSync(registerFile(featureDir))) {
|
|
351
351
|
return { ...base, finding: 'no-register', message: `${featureDir} has no decisions/register.json; nothing was touched` };
|
|
@@ -360,7 +360,7 @@ function compactDecisions({ featureDir, slug, repoRoot, paths, now = new Date(),
|
|
|
360
360
|
if (result.failed.length) {
|
|
361
361
|
return {
|
|
362
362
|
...base, status: result.status, finding: 'compaction-failed',
|
|
363
|
-
message: result.failed.map((f
|
|
363
|
+
message: result.failed.map((f ) => f.message).join('; '),
|
|
364
364
|
artifact: result.failed[0].path, moved: result.moved, failed: result.failed,
|
|
365
365
|
};
|
|
366
366
|
}
|
|
@@ -374,17 +374,17 @@ function compactDecisions({ featureDir, slug, repoRoot, paths, now = new Date(),
|
|
|
374
374
|
|
|
375
375
|
const VALID_ACTIONS = ['init', 'add', 'list', 'compact'];
|
|
376
376
|
|
|
377
|
-
function readBatchFile(batchPath
|
|
378
|
-
let text
|
|
377
|
+
function readBatchFile(batchPath , fsImpl ) {
|
|
378
|
+
let text ;
|
|
379
379
|
try {
|
|
380
380
|
// `-` is stdin, as for the evidence verb.
|
|
381
381
|
text = batchPath === '-' ? fsImpl.readFileSync(0, 'utf8') : fsImpl.readFileSync(path.resolve(batchPath), 'utf8');
|
|
382
|
-
} catch (error
|
|
382
|
+
} catch (error ) {
|
|
383
383
|
throw new DecisionUsageError(`cannot read --batch ${batchPath}: ${error.message}`);
|
|
384
384
|
}
|
|
385
385
|
try {
|
|
386
386
|
return JSON.parse(text);
|
|
387
|
-
} catch (error
|
|
387
|
+
} catch (error ) {
|
|
388
388
|
throw new DecisionUsageError(`--batch ${batchPath} is not valid JSON: ${error.message}`);
|
|
389
389
|
}
|
|
390
390
|
}
|
|
@@ -401,15 +401,15 @@ function readBatchFile(batchPath: string, fsImpl: any): any {
|
|
|
401
401
|
* refs, source, batch (path); list: all
|
|
402
402
|
* @returns {{exitCode:number, result?:Object, usage?:string}}
|
|
403
403
|
*/
|
|
404
|
-
function runDecision({ action = 'list', projectRoot, slug = null, flags = {}, now = new Date(), fsImpl = nodeFs }
|
|
404
|
+
function runDecision({ action = 'list', projectRoot, slug = null, flags = {}, now = new Date(), fsImpl = nodeFs } ) {
|
|
405
405
|
const act = action === 'status' ? 'list' : action;
|
|
406
|
-
let resolved
|
|
406
|
+
let resolved = null;
|
|
407
407
|
try {
|
|
408
408
|
if (!VALID_ACTIONS.includes(act)) throw new DecisionUsageError(`unknown --action '${action}'. Valid: ${VALID_ACTIONS.join(', ')}`);
|
|
409
409
|
const feature = resolveActiveFeature({ projectRoot, slug });
|
|
410
410
|
if (feature.error) throw new DecisionUsageError(feature.message);
|
|
411
411
|
resolved = { featureDir: feature.featureDir, slug: feature.paths.feature_slug };
|
|
412
|
-
let result
|
|
412
|
+
let result ;
|
|
413
413
|
if (act === 'init') {
|
|
414
414
|
result = initRegister({ ...resolved, fsImpl });
|
|
415
415
|
} else if (act === 'list') {
|
|
@@ -417,13 +417,13 @@ function runDecision({ action = 'list', projectRoot, slug = null, flags = {}, no
|
|
|
417
417
|
} else if (act === 'compact') {
|
|
418
418
|
result = compactDecisions({ ...resolved, repoRoot: feature.repoRoot, paths: feature.paths, now, fsImpl });
|
|
419
419
|
} else {
|
|
420
|
-
let items
|
|
420
|
+
let items ;
|
|
421
421
|
if (flags.batch) {
|
|
422
422
|
const named = ITEM_KEYS.filter((k) => flags[k] !== undefined);
|
|
423
423
|
if (named.length) throw new DecisionUsageError(`--batch cannot be combined with --${named[0]}`);
|
|
424
424
|
items = readBatchFile(flags.batch, fsImpl);
|
|
425
425
|
} else {
|
|
426
|
-
const single
|
|
426
|
+
const single = {};
|
|
427
427
|
for (const key of ITEM_KEYS) if (flags[key] !== undefined) single[key] = flags[key];
|
|
428
428
|
for (const key of REQUIRED_KEYS) {
|
|
429
429
|
if (single[key] === undefined) throw new DecisionUsageError(`--${key} is required for --action add (or pass --batch <file.json>)`);
|
|
@@ -433,7 +433,7 @@ function runDecision({ action = 'list', projectRoot, slug = null, flags = {}, no
|
|
|
433
433
|
result = addDecisions({ ...resolved, items, now, fsImpl });
|
|
434
434
|
}
|
|
435
435
|
return { exitCode: result.finding ? 1 : 0, result };
|
|
436
|
-
} catch (error
|
|
436
|
+
} catch (error ) {
|
|
437
437
|
if (error instanceof DecisionUsageError) return { exitCode: 2, usage: error.message };
|
|
438
438
|
// Another writer holds the register lock: a condition to retry, not a mistake in the call.
|
|
439
439
|
if (resolved && error && /^Could not lock/.test(error.message || '')) {
|
|
@@ -443,21 +443,21 @@ function runDecision({ action = 'list', projectRoot, slug = null, flags = {}, no
|
|
|
443
443
|
}
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
-
function humanLines(result
|
|
447
|
-
const lines
|
|
446
|
+
function humanLines(result ) {
|
|
447
|
+
const lines = [];
|
|
448
448
|
if (result.finding) lines.push(`${result.finding}: ${result.message}`);
|
|
449
449
|
if (result.action === 'init' && !result.finding) {
|
|
450
450
|
lines.push(result.created ? `created ${path.join(result.featureDir, 'decisions', 'register.json')}` : 'register already exists; nothing written');
|
|
451
451
|
}
|
|
452
|
-
for (const a of (result.added || [])
|
|
453
|
-
for (const s of (result.superseded || [])
|
|
452
|
+
for (const a of (result.added || []) ) lines.push(`added ${a.id} (${a.topic})`);
|
|
453
|
+
for (const s of (result.superseded || []) ) lines.push(`superseded ${s.id} by ${s.by}`);
|
|
454
454
|
if (result.action === 'list' && !result.finding) {
|
|
455
455
|
if (result.decisions.length === 0) lines.push('No decisions.');
|
|
456
|
-
for (const d of result.decisions
|
|
456
|
+
for (const d of result.decisions ) lines.push(`${d.id} ${d.topic} ${d.statement} [${d.decidedBy}, ${d.stage}${d.status === 'live' ? '' : `, superseded by ${d.supersededBy}`}]`);
|
|
457
457
|
}
|
|
458
458
|
if (result.action === 'compact') {
|
|
459
459
|
if (result.status === 'unchanged') lines.push('nothing to compact');
|
|
460
|
-
for (const m of (result.moved || [])
|
|
460
|
+
for (const m of (result.moved || []) ) lines.push(`moved ${m.lines} line${m.lines === 1 ? '' : 's'} from ${m.path} to ${m.archive}`);
|
|
461
461
|
}
|
|
462
462
|
return lines;
|
|
463
463
|
}
|
|
@@ -466,7 +466,7 @@ function humanLines(result: any): string[] {
|
|
|
466
466
|
* `decision` verb. Exit 0 = answered, 1 = a finding the caller must act on, 2 = usage.
|
|
467
467
|
* `--json` prints the result object unmodified.
|
|
468
468
|
*/
|
|
469
|
-
function handleDecisionCommand({ action = 'list', projectRoot, slug = null, flags = {}, json = false }
|
|
469
|
+
function handleDecisionCommand({ action = 'list', projectRoot, slug = null, flags = {}, json = false } = {}) {
|
|
470
470
|
const run = runDecision({ action, projectRoot: projectRoot || process.cwd(), slug, flags });
|
|
471
471
|
if (run.usage !== undefined) return usageError('decision', run.usage, json);
|
|
472
472
|
if (json) console.log(JSON.stringify(run.result, null, 2));
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
/** The names a retrieval score arrives under, in camelCase (`--relevance-score` normalises here).
|
|
6
|
+
* Exported because `bin/doflow.js` refuses the same names on the command line, and because
|
|
7
|
+
* `retrieval-plan.ts` and `outcome.ts` refuse them on the option object a declared plan and a
|
|
8
|
+
* terminal record are built from: one definition, three input surfaces (argv, a `--batch` JSON
|
|
9
|
+
* file, and a verb's own options), each with exactly one enforcement point. A second copy of
|
|
10
|
+
* this list is how one of them drifts. */
|
|
11
|
+
const EVIDENCE_SCORE_FIELDS = new Set([
|
|
12
|
+
'confidence', 'score', 'relevance', 'relevanceScore', 'similarity',
|
|
13
|
+
'certainty', 'probability', 'rank', 'distance', 'weight',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
/** The one refusal message for a score offered as a property of a fact. */
|
|
17
|
+
function scoreFieldRefusal(name ) {
|
|
18
|
+
return `'${name}' is a retrieval score and this boundary records none (FR-007). Relevance is a `
|
|
19
|
+
+ 'property of a search, not of a fact: stored beside the fact it is read as confidence by the '
|
|
20
|
+
+ 'next worker, which is the specific interpretation the requirement forbids. Record the '
|
|
21
|
+
+ 'locator and the content instead — a reader who can see where a fact came from does not need '
|
|
22
|
+
+ 'to be told how sure to be.';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Refuses a score-shaped key anywhere in an item, including nested under source or locator. */
|
|
26
|
+
function assertNoScoreFields(value , prefix ) {
|
|
27
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return;
|
|
28
|
+
for (const [key, inner] of Object.entries(value )) {
|
|
29
|
+
const dotted = prefix ? `${prefix}.${key}` : key;
|
|
30
|
+
if (EVIDENCE_SCORE_FIELDS.has(key)) throw new Error(scoreFieldRefusal(dotted));
|
|
31
|
+
assertNoScoreFields(inner, dotted);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
EVIDENCE_SCORE_FIELDS,
|
|
37
|
+
scoreFieldRefusal,
|
|
38
|
+
assertNoScoreFields,
|
|
39
|
+
};
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
const fs = require('node:fs');
|
|
6
6
|
const path = require('node:path');
|
|
7
7
|
const crypto = require('node:crypto');
|
|
8
|
-
const { REPO_ROOT } = require('../helper/repo-root.
|
|
9
|
-
const { updateTaskState, readTaskState, mergeRecords } = require('./task-state.
|
|
10
|
-
const { taskStoreDir } = require('./task-scope.
|
|
8
|
+
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
9
|
+
const { updateTaskState, readTaskState, mergeRecords } = require('./task-state.js');
|
|
10
|
+
const { taskStoreDir } = require('./task-scope.js');
|
|
11
|
+
const { assertNoScoreFields } = require('./evidence-fields.js');
|
|
11
12
|
|
|
12
13
|
const VALID_EVIDENCE_KINDS = new Set([
|
|
13
14
|
'exact-search',
|
|
@@ -47,7 +48,7 @@ const EVIDENCE_STATUSES = new Set(['active', 'superseded']);
|
|
|
47
48
|
* @param {string} file the ledger path, so the message says which file to look at
|
|
48
49
|
* @returns {object} the same item, once its status is one of the two
|
|
49
50
|
*/
|
|
50
|
-
function assertKnownStatus(item
|
|
51
|
+
function assertKnownStatus(item , file ) {
|
|
51
52
|
const status = item?.status;
|
|
52
53
|
if (!EVIDENCE_STATUSES.has(status)) {
|
|
53
54
|
throw new Error(
|
|
@@ -66,7 +67,7 @@ function assertKnownStatus(item: any, file: string) {
|
|
|
66
67
|
* @param {string} taskId
|
|
67
68
|
* @returns {string} the validated id, for use in an expression
|
|
68
69
|
*/
|
|
69
|
-
function assertSafeTaskId(taskId
|
|
70
|
+
function assertSafeTaskId(taskId ) {
|
|
70
71
|
if (typeof taskId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(taskId) || taskId.includes('..')) {
|
|
71
72
|
throw new Error(`Invalid task id '${taskId}': expected letters, digits, dot, dash or underscore, and no path separators`);
|
|
72
73
|
}
|
|
@@ -74,14 +75,14 @@ function assertSafeTaskId(taskId: string): string {
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
class EvidenceLedger {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
85
86
|
/**
|
|
86
87
|
* @param {Object} [options]
|
|
87
88
|
* @param {string} [options.repoRoot]
|
|
@@ -89,7 +90,7 @@ class EvidenceLedger {
|
|
|
89
90
|
* @param {string} [options.slug] feature whose namespace the records live under (see task-scope.js)
|
|
90
91
|
* @param {Object} [options.fsImpl]
|
|
91
92
|
*/
|
|
92
|
-
constructor(options
|
|
93
|
+
constructor(options = {}) {
|
|
93
94
|
this.fsImpl = options.fsImpl || fs;
|
|
94
95
|
this.repoRoot = options.repoRoot || REPO_ROOT;
|
|
95
96
|
this.fixedStateDir = options.stateDir || null;
|
|
@@ -101,7 +102,7 @@ class EvidenceLedger {
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
/** Where this task's evidence file lives: the feature's namespace when it has one (IC-002). */
|
|
104
|
-
fileFor(taskId
|
|
105
|
+
fileFor(taskId ) {
|
|
105
106
|
const dir = this.fixedStateDir
|
|
106
107
|
|| taskStoreDir({ projectRoot: this.repoRoot, store: 'evidence', taskId, slug: this.slug });
|
|
107
108
|
return path.join(dir, `${assertSafeTaskId(taskId)}.json`);
|
|
@@ -115,7 +116,7 @@ class EvidenceLedger {
|
|
|
115
116
|
* scheme. The random component carries that burden; timestamp and sequence stay for readability.
|
|
116
117
|
* @returns {string}
|
|
117
118
|
*/
|
|
118
|
-
generateId()
|
|
119
|
+
generateId() {
|
|
119
120
|
this.seq += 1;
|
|
120
121
|
return `ev_${Date.now().toString(36)}_${crypto.randomBytes(4).toString('hex')}_${this.seq.toString(36)}`;
|
|
121
122
|
}
|
|
@@ -125,10 +126,11 @@ class EvidenceLedger {
|
|
|
125
126
|
* @param {Object} item
|
|
126
127
|
* @returns {string} evidenceId
|
|
127
128
|
*/
|
|
128
|
-
addEvidence(item
|
|
129
|
+
addEvidence(item ) {
|
|
129
130
|
if (!item || typeof item !== 'object') {
|
|
130
131
|
throw new Error('Evidence item must be an object');
|
|
131
132
|
}
|
|
133
|
+
assertNoScoreFields(item, '');
|
|
132
134
|
|
|
133
135
|
if (!item.kind || !VALID_EVIDENCE_KINDS.has(item.kind)) {
|
|
134
136
|
throw new Error(`Invalid evidence kind '${item.kind}'. Allowed: ${[...VALID_EVIDENCE_KINDS].join(', ')}`);
|
|
@@ -152,6 +154,8 @@ class EvidenceLedger {
|
|
|
152
154
|
fileHash: item.freshness?.fileHash || null,
|
|
153
155
|
observedAt: item.freshness?.observedAt || new Date().toISOString(),
|
|
154
156
|
status: item.freshness?.status || 'FRESH',
|
|
157
|
+
isDirty: item.freshness?.isDirty !== undefined ? item.freshness.isDirty : null,
|
|
158
|
+
...(item.freshness?.treeFingerprint ? { treeFingerprint: item.freshness.treeFingerprint } : {}),
|
|
155
159
|
};
|
|
156
160
|
|
|
157
161
|
const evidenceRecord = {
|
|
@@ -163,6 +167,7 @@ class EvidenceLedger {
|
|
|
163
167
|
provenance,
|
|
164
168
|
content: item.content || null,
|
|
165
169
|
freshness,
|
|
170
|
+
...(item.result !== undefined ? { result: item.result } : {}),
|
|
166
171
|
// Which template requirements this item claims to establish. Readiness counts an item toward
|
|
167
172
|
// a requirement only when the writer named it here — kind-matching alone let an item saying
|
|
168
173
|
// "tests failed" satisfy a passing-baseline requirement (architecture review R1, P1).
|
|
@@ -190,7 +195,7 @@ class EvidenceLedger {
|
|
|
190
195
|
* @param {string} id
|
|
191
196
|
* @returns {Object|null}
|
|
192
197
|
*/
|
|
193
|
-
getEvidence(id
|
|
198
|
+
getEvidence(id ) {
|
|
194
199
|
return this.evidenceMap.get(id) || null;
|
|
195
200
|
}
|
|
196
201
|
|
|
@@ -199,7 +204,7 @@ class EvidenceLedger {
|
|
|
199
204
|
* @param {Object} [filter={}]
|
|
200
205
|
* @returns {Array<Object>}
|
|
201
206
|
*/
|
|
202
|
-
queryEvidence(filter
|
|
207
|
+
queryEvidence(filter = {}) {
|
|
203
208
|
const results = [];
|
|
204
209
|
for (const item of this.evidenceMap.values()) {
|
|
205
210
|
if (filter.taskId && item.taskId !== filter.taskId) continue;
|
|
@@ -208,6 +213,8 @@ class EvidenceLedger {
|
|
|
208
213
|
if (filter.capability && item.source.capability !== filter.capability) continue;
|
|
209
214
|
if (filter.file && item.locator.file !== filter.file) continue;
|
|
210
215
|
if (filter.status && item.freshness.status !== filter.status) continue;
|
|
216
|
+
if (filter.queryRef && item.source?.queryRef !== filter.queryRef) continue;
|
|
217
|
+
if (filter.attemptRef && item.source?.attemptRef !== filter.attemptRef) continue;
|
|
211
218
|
results.push(item);
|
|
212
219
|
}
|
|
213
220
|
return results;
|
|
@@ -225,7 +232,7 @@ class EvidenceLedger {
|
|
|
225
232
|
* @param {string} replacedBy the id of the item that supersedes it; must already exist
|
|
226
233
|
* @returns {string} the new status
|
|
227
234
|
*/
|
|
228
|
-
supersedeEvidence(evidenceId
|
|
235
|
+
supersedeEvidence(evidenceId , replacedBy ) {
|
|
229
236
|
if (!replacedBy) {
|
|
230
237
|
throw new Error('supersede requires the id of the evidence item that replaces this one');
|
|
231
238
|
}
|
|
@@ -254,7 +261,7 @@ class EvidenceLedger {
|
|
|
254
261
|
* @param {Array<string>} modifiedFiles
|
|
255
262
|
* @returns {number} count of invalidated items
|
|
256
263
|
*/
|
|
257
|
-
invalidateFiles(modifiedFiles
|
|
264
|
+
invalidateFiles(modifiedFiles ) {
|
|
258
265
|
if (!Array.isArray(modifiedFiles) || modifiedFiles.length === 0) return 0;
|
|
259
266
|
const fileSet = new Set(modifiedFiles);
|
|
260
267
|
let count = 0;
|
|
@@ -283,12 +290,12 @@ class EvidenceLedger {
|
|
|
283
290
|
* @param {string} [taskId='default']
|
|
284
291
|
* @returns {string} filePath
|
|
285
292
|
*/
|
|
286
|
-
save(taskId = 'default')
|
|
293
|
+
save(taskId = 'default') {
|
|
287
294
|
const targetFile = this.fileFor(taskId);
|
|
288
|
-
let merged
|
|
295
|
+
let merged ;
|
|
289
296
|
const file = updateTaskState({
|
|
290
297
|
fsImpl: this.fsImpl, file: targetFile,
|
|
291
|
-
build: (disk
|
|
298
|
+
build: (disk ) => {
|
|
292
299
|
merged = mergeRecords(this.queryEvidence({ taskId }), disk?.evidence || [], this.baseline);
|
|
293
300
|
return { taskId, updatedAt: new Date().toISOString(), evidenceCount: merged.length, evidence: merged };
|
|
294
301
|
},
|
|
@@ -305,7 +312,7 @@ class EvidenceLedger {
|
|
|
305
312
|
* @param {string} [taskId='default']
|
|
306
313
|
* @returns {number} count of loaded items
|
|
307
314
|
*/
|
|
308
|
-
load(taskId = 'default')
|
|
315
|
+
load(taskId = 'default') {
|
|
309
316
|
const targetFile = this.fileFor(taskId);
|
|
310
317
|
const data = readTaskState(this.fsImpl, targetFile);
|
|
311
318
|
if (data && Array.isArray(data.evidence)) {
|