@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
|
@@ -38,18 +38,17 @@
|
|
|
38
38
|
* emits are counts of records.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const {
|
|
44
|
-
const {
|
|
45
|
-
const {
|
|
46
|
-
const { probeFreshness } = require('./health.ts');
|
|
41
|
+
const { CapabilityRouter } = require('./capability-router.js');
|
|
42
|
+
const { EvidenceLedger, assertSafeTaskId } = require('./evidence-ledger.js');
|
|
43
|
+
const { FreshnessValidator } = require('./freshness.js');
|
|
44
|
+
const { probeFreshness } = require('./health.js');
|
|
45
|
+
const { planPath, readPlan, updatePlan, moveCorruptPlanAside, mintPlanId, activeAttemptRef, resolveNeed, bindEvidence, deriveOutcome } = require('./retrieval-record.js');
|
|
47
46
|
// One definition of the score refusal, applied at a second boundary. See the note on
|
|
48
|
-
// EVIDENCE_SCORE_FIELDS in
|
|
47
|
+
// EVIDENCE_SCORE_FIELDS in src/runtime/evidence-fields.js: relevance is a property of a search, not of a fact, and a plan
|
|
49
48
|
// that recorded one would hand the next worker a confidence dressed as a measurement.
|
|
50
|
-
const { assertNoScoreFields } = require('./
|
|
51
|
-
const { finishRuntime, usageError } = require('./cli-result.
|
|
52
|
-
const { REPO_ROOT } = require('../helper/repo-root.
|
|
49
|
+
const { assertNoScoreFields } = require('./evidence-fields.js');
|
|
50
|
+
const { finishRuntime, usageError } = require('./cli-result.js');
|
|
51
|
+
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
53
52
|
|
|
54
53
|
// ── the closed result vocabulary (design §4) ─────────────────────────────────────────────────
|
|
55
54
|
|
|
@@ -73,6 +72,18 @@ const RESULTS = Object.freeze([RETRIEVED, EMPTY, UNREACHED, UNVERIFIED]);
|
|
|
73
72
|
const NOT_APPLICABLE = 'NOT_APPLICABLE';
|
|
74
73
|
const FRESHNESS_STATES = Object.freeze(['FRESH', 'STALE', 'UNKNOWN', NOT_APPLICABLE]);
|
|
75
74
|
|
|
75
|
+
// ── attempt reason codes (IC-013) ─────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
const ATTEMPT_REASON_CODES = Object.freeze([
|
|
78
|
+
'provider-unavailable',
|
|
79
|
+
'policy-bypass',
|
|
80
|
+
'provider-error',
|
|
81
|
+
'timeout',
|
|
82
|
+
'empty-answer',
|
|
83
|
+
'index-stale',
|
|
84
|
+
'index-unverifiable',
|
|
85
|
+
]);
|
|
86
|
+
|
|
76
87
|
/**
|
|
77
88
|
* Design §4's mapping table, as data. It maps only the *empty* answer: a provider that returned
|
|
78
89
|
* something is RETRIEVED whatever the age of its index, because the thing it returned is a
|
|
@@ -84,19 +95,26 @@ const FRESHNESS_STATES = Object.freeze(['FRESH', 'STALE', 'UNKNOWN', NOT_APPLICA
|
|
|
84
95
|
* this module exists to prevent — so staleness travels as a marker beside `EMPTY`, and the
|
|
85
96
|
* provider's `refresh` command travels with it so the remedy is one step away.
|
|
86
97
|
*/
|
|
87
|
-
const EMPTY_ANSWER_BY_FRESHNESS
|
|
98
|
+
const EMPTY_ANSWER_BY_FRESHNESS = Object.freeze({
|
|
88
99
|
UNKNOWN: UNVERIFIED,
|
|
89
100
|
STALE: EMPTY,
|
|
90
101
|
FRESH: EMPTY,
|
|
91
102
|
[NOT_APPLICABLE]: EMPTY,
|
|
92
103
|
});
|
|
93
104
|
|
|
105
|
+
function requirePlanForUpdate(disk , taskId ) {
|
|
106
|
+
if (disk === null) {
|
|
107
|
+
throw new Error(`the retrieval plan for task ${taskId} was removed during the update; re-declare it`);
|
|
108
|
+
}
|
|
109
|
+
return disk;
|
|
110
|
+
}
|
|
111
|
+
|
|
94
112
|
/**
|
|
95
113
|
* What an empty answer becomes, given the freshness of the index behind it.
|
|
96
114
|
* @param {string} freshnessState one of FRESHNESS_STATES
|
|
97
115
|
* @returns {string} one of RESULTS
|
|
98
116
|
*/
|
|
99
|
-
function resultForEmptyAnswer(freshnessState
|
|
117
|
+
function resultForEmptyAnswer(freshnessState ) {
|
|
100
118
|
const mapped = EMPTY_ANSWER_BY_FRESHNESS[freshnessState];
|
|
101
119
|
if (!mapped) {
|
|
102
120
|
// Fail closed rather than pick a default: an unrecognised freshness token means the record was
|
|
@@ -107,50 +125,6 @@ function resultForEmptyAnswer(freshnessState: any) {
|
|
|
107
125
|
return mapped;
|
|
108
126
|
}
|
|
109
127
|
|
|
110
|
-
// ── the record ────────────────────────────────────────────────────────────────────────────────
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Where a task's plan lives, under the invoking project's state root.
|
|
114
|
-
*
|
|
115
|
-
* The task id is validated by the evidence ledger's own rule, unchanged: it becomes a filename, so
|
|
116
|
-
* it must never be able to name a path.
|
|
117
|
-
* @param {string} projectRoot
|
|
118
|
-
* @param {string} taskId
|
|
119
|
-
* @param {string|null} [slug] the feature the task belongs to, when not the branch's (task-scope.js)
|
|
120
|
-
* @returns {string}
|
|
121
|
-
*/
|
|
122
|
-
function planPath(projectRoot: any, taskId: any, slug: any = null) {
|
|
123
|
-
const dir = taskStoreDir({ projectRoot, store: 'retrieval', taskId: assertSafeTaskId(taskId), slug });
|
|
124
|
-
return path.join(dir, `${taskId}.json`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* @param {string} projectRoot
|
|
129
|
-
* @param {string} taskId
|
|
130
|
-
* @returns {Object|null} the record, or null when no plan was declared
|
|
131
|
-
*/
|
|
132
|
-
function readPlan(projectRoot: any, taskId: any, slug: any = null) {
|
|
133
|
-
const file = planPath(projectRoot, taskId, slug);
|
|
134
|
-
if (!fs.existsSync(file)) return null;
|
|
135
|
-
try {
|
|
136
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
137
|
-
} catch (error: any) {
|
|
138
|
-
throw new Error(`the retrieval plan at '${file}' is not readable: ${error.message}`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* @param {string} projectRoot
|
|
144
|
-
* @param {string} taskId
|
|
145
|
-
* @param {Object} record
|
|
146
|
-
* @returns {string} the file written
|
|
147
|
-
*/
|
|
148
|
-
function writePlan(projectRoot: any, taskId: any, record: any, slug: any = null) {
|
|
149
|
-
const file = planPath(projectRoot, taskId, slug);
|
|
150
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
151
|
-
fs.writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
|
|
152
|
-
return file;
|
|
153
|
-
}
|
|
154
128
|
|
|
155
129
|
/**
|
|
156
130
|
* The `sourceMode` each provider id declares, as a lookup the plan builds once per declare.
|
|
@@ -161,16 +135,16 @@ function writePlan(projectRoot: any, taskId: any, record: any, slug: any = null)
|
|
|
161
135
|
* @param {Object} capabilities the router's `capabilities` map
|
|
162
136
|
* @returns {(providerId: string) => string|null} the declared mode, or null for an unknown id
|
|
163
137
|
*/
|
|
164
|
-
function sourceModeLookup(capabilities
|
|
138
|
+
function sourceModeLookup(capabilities ) {
|
|
165
139
|
const modes = new Map();
|
|
166
|
-
for (const capability of Object.values
|
|
140
|
+
for (const capability of Object.values (capabilities || {})) {
|
|
167
141
|
for (const provider of (capability && capability.providers) || []) {
|
|
168
142
|
if (!provider || !provider.id) continue;
|
|
169
143
|
if (modes.get(provider.id) === 'index') continue;
|
|
170
144
|
modes.set(provider.id, provider.sourceMode || null);
|
|
171
145
|
}
|
|
172
146
|
}
|
|
173
|
-
return (providerId
|
|
147
|
+
return (providerId ) => (modes.has(providerId) ? modes.get(providerId) : null);
|
|
174
148
|
}
|
|
175
149
|
|
|
176
150
|
/**
|
|
@@ -186,8 +160,8 @@ function sourceModeLookup(capabilities: any) {
|
|
|
186
160
|
* absent, every provider without an artifact layout is treated as unknown (fail closed)
|
|
187
161
|
* @returns {{state: string, artifact: string|null, refresh: string|null, label: string|null, reason: string|null}}
|
|
188
162
|
*/
|
|
189
|
-
function freshnessEntry(providerId
|
|
190
|
-
const probe = probeFreshness(providerId, { root:
|
|
163
|
+
function freshnessEntry(providerId , sourceRoot , sourceModeOf = () => null) {
|
|
164
|
+
const probe = probeFreshness(providerId, { root: sourceRoot });
|
|
191
165
|
if (!probe) {
|
|
192
166
|
// `probeFreshness` returns null for a provider it has no artifact layout for. What that means
|
|
193
167
|
// depends on how the provider answers: a `live` provider reads current files or commands, so
|
|
@@ -230,9 +204,9 @@ function freshnessEntry(providerId: any, projectRoot: any, sourceModeOf: any = (
|
|
|
230
204
|
* @param {Object} need
|
|
231
205
|
* @returns {string} one of FRESHNESS_STATES
|
|
232
206
|
*/
|
|
233
|
-
function freshnessOf(record
|
|
207
|
+
function freshnessOf(record , need ) {
|
|
234
208
|
if (!need.provider) return NOT_APPLICABLE;
|
|
235
|
-
const entry
|
|
209
|
+
const entry = record.providers && record.providers[need.provider];
|
|
236
210
|
return entry && entry.state ? entry.state : NOT_APPLICABLE;
|
|
237
211
|
}
|
|
238
212
|
|
|
@@ -248,14 +222,14 @@ function freshnessOf(record: any, need: any) {
|
|
|
248
222
|
* @param {Array<Object>} items
|
|
249
223
|
* @returns {Array<string>} evidence ids
|
|
250
224
|
*/
|
|
251
|
-
function evidenceIdsFor(need
|
|
225
|
+
function evidenceIdsFor(need , items ) {
|
|
252
226
|
return items
|
|
253
|
-
.filter((item
|
|
227
|
+
.filter((item ) => {
|
|
254
228
|
const source = item.source || {};
|
|
255
229
|
if (need.provider && source.provider === need.provider) return true;
|
|
256
230
|
return Boolean(need.capability) && source.capability === need.capability;
|
|
257
231
|
})
|
|
258
|
-
.map((item
|
|
232
|
+
.map((item ) => item.id);
|
|
259
233
|
}
|
|
260
234
|
|
|
261
235
|
// ── actions ───────────────────────────────────────────────────────────────────────────────────
|
|
@@ -274,13 +248,13 @@ function evidenceIdsFor(need: any, items: any) {
|
|
|
274
248
|
*
|
|
275
249
|
* @returns {number} exit code
|
|
276
250
|
*/
|
|
277
|
-
function declarePlan({ router, projectRoot, taskId, stage, intents, json, slug }
|
|
278
|
-
const needs
|
|
251
|
+
function declarePlan({ router, projectRoot, sourceRoot, taskId, stage, intents, json, slug } ) {
|
|
252
|
+
const needs = [];
|
|
279
253
|
for (const intent of intents) {
|
|
280
254
|
let resolution;
|
|
281
255
|
try {
|
|
282
256
|
resolution = router.resolveIntent(intent);
|
|
283
|
-
} catch (error
|
|
257
|
+
} catch (error ) {
|
|
284
258
|
// A misspelled intent is a caller error, not an unresolvable need. Recording it as
|
|
285
259
|
// declared-unresolvable would file a typo as a machine limitation and hide it until report.
|
|
286
260
|
return usageError('retrieval-plan',
|
|
@@ -299,22 +273,39 @@ function declarePlan({ router, projectRoot, taskId, stage, intents, json, slug }
|
|
|
299
273
|
|
|
300
274
|
// R8, in one loop: the probe set is the set of DISTINCT providers, so cost is bounded by
|
|
301
275
|
// provider count and not by need count. Six needs across two providers pay two scans.
|
|
302
|
-
const providers
|
|
303
|
-
const sourceModeOf
|
|
276
|
+
const providers = {};
|
|
277
|
+
const sourceModeOf = sourceModeLookup(router.capabilities);
|
|
304
278
|
for (const need of needs) {
|
|
305
279
|
if (!need.provider || providers[need.provider]) continue;
|
|
306
|
-
providers[need.provider] = freshnessEntry(need.provider,
|
|
280
|
+
providers[need.provider] = freshnessEntry(need.provider, sourceRoot, sourceModeOf);
|
|
307
281
|
}
|
|
308
282
|
|
|
309
|
-
const
|
|
283
|
+
const planId = mintPlanId();
|
|
284
|
+
const attemptRef = activeAttemptRef(projectRoot, taskId, slug);
|
|
285
|
+
for (const need of needs) {
|
|
286
|
+
need.queryRef = `${planId}#${need.intent}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const record = {
|
|
310
290
|
version: 1,
|
|
291
|
+
planId,
|
|
292
|
+
binding: 'query',
|
|
311
293
|
taskId,
|
|
312
294
|
stage: stage || null,
|
|
313
295
|
declaredAt: new Date().toISOString(),
|
|
296
|
+
attemptRef,
|
|
314
297
|
providers,
|
|
315
298
|
needs,
|
|
316
299
|
};
|
|
317
|
-
|
|
300
|
+
let file ;
|
|
301
|
+
try {
|
|
302
|
+
file = updatePlan(projectRoot, taskId, slug, () => record, undefined, () => {
|
|
303
|
+
const corruptFile = moveCorruptPlanAside(projectRoot, taskId, slug);
|
|
304
|
+
if (corruptFile) console.log(`Moved unreadable retrieval plan to ${corruptFile}; declaring a new plan.`);
|
|
305
|
+
});
|
|
306
|
+
} catch (error ) {
|
|
307
|
+
return usageError('retrieval-plan', error.message, json, error);
|
|
308
|
+
}
|
|
318
309
|
|
|
319
310
|
const unresolvable = needs.filter((need) => !need.provider).map((need) => need.intent);
|
|
320
311
|
if (json) {
|
|
@@ -326,12 +317,13 @@ function declarePlan({ router, projectRoot, taskId, stage, intents, json, slug }
|
|
|
326
317
|
console.log('═'.repeat(78));
|
|
327
318
|
console.log(`Stage: ${record.stage || 'not stated'}`);
|
|
328
319
|
console.log('─'.repeat(78));
|
|
329
|
-
console.log('Intent'.padEnd(26) + 'Provider'.padEnd(22) + 'Index');
|
|
320
|
+
console.log('Intent'.padEnd(26) + 'Provider'.padEnd(22) + 'Index'.padEnd(16) + 'QueryRef');
|
|
330
321
|
for (const need of needs) {
|
|
331
322
|
console.log(
|
|
332
323
|
need.intent.padEnd(26)
|
|
333
324
|
+ (need.provider || 'none — unresolvable').padEnd(22)
|
|
334
|
-
+ freshnessOf(record, need)
|
|
325
|
+
+ freshnessOf(record, need).padEnd(16)
|
|
326
|
+
+ (need.queryRef || ''),
|
|
335
327
|
);
|
|
336
328
|
}
|
|
337
329
|
printProviderIndexes(record);
|
|
@@ -367,11 +359,11 @@ function declarePlan({ router, projectRoot, taskId, stage, intents, json, slug }
|
|
|
367
359
|
*
|
|
368
360
|
* @returns {number} exit code
|
|
369
361
|
*/
|
|
370
|
-
function reportPlan({ projectRoot, taskId, reached, json, slug }
|
|
371
|
-
let record;
|
|
362
|
+
function reportPlan({ projectRoot, sourceRoot, taskId, reached, json, slug, repoRoot } ) {
|
|
363
|
+
let record ;
|
|
372
364
|
try {
|
|
373
365
|
record = readPlan(projectRoot, taskId, slug);
|
|
374
|
-
} catch (error
|
|
366
|
+
} catch (error ) {
|
|
375
367
|
return usageError('retrieval-plan', error.message, json, error);
|
|
376
368
|
}
|
|
377
369
|
if (!record) {
|
|
@@ -381,7 +373,7 @@ function reportPlan({ projectRoot, taskId, reached, json, slug }: any) {
|
|
|
381
373
|
+ 'plan would state that nothing was missed, which is not something an absent plan can establish', json);
|
|
382
374
|
}
|
|
383
375
|
|
|
384
|
-
const declared = new Set(record.needs.map((need
|
|
376
|
+
const declared = new Set(record.needs.map((need ) => need.intent));
|
|
385
377
|
const undeclared = [...reached].filter((intent) => !declared.has(intent)).sort();
|
|
386
378
|
if (undeclared.length > 0) {
|
|
387
379
|
return usageError('retrieval-plan',
|
|
@@ -392,25 +384,129 @@ function reportPlan({ projectRoot, taskId, reached, json, slug }: any) {
|
|
|
392
384
|
const ledger = new EvidenceLedger({ repoRoot: projectRoot, slug });
|
|
393
385
|
try {
|
|
394
386
|
ledger.load(taskId);
|
|
395
|
-
} catch (error
|
|
387
|
+
} catch (error ) {
|
|
396
388
|
return usageError('retrieval-plan', error.message, json, error);
|
|
397
389
|
}
|
|
390
|
+
new FreshnessValidator({ repoRoot: projectRoot, treeRoot: sourceRoot }).validateLedgerFreshness(ledger);
|
|
398
391
|
const items = ledger.queryEvidence({ taskId });
|
|
399
392
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
393
|
+
const isQueryBound = Boolean(record.planId);
|
|
394
|
+
let binding = isQueryBound ? bindEvidence(record, items) : null;
|
|
395
|
+
let reportSourceModeOf = null;
|
|
396
|
+
const probeByProvider = new Map ();
|
|
397
|
+
if (record.planId) {
|
|
398
|
+
const initialBinding = bindEvidence(record, items);
|
|
399
|
+
const missingProviders = new Set ();
|
|
400
|
+
for (const need of record.needs) {
|
|
401
|
+
for (const item of initialBinding.byNeed.get(need.intent) || []) {
|
|
402
|
+
if (item.result !== 'EMPTY' && !record.providers?.[item.source?.provider]) {
|
|
403
|
+
if (item.source?.provider) missingProviders.add(item.source.provider);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (missingProviders.size > 0) {
|
|
408
|
+
const reportRouter = new CapabilityRouter({ repoRoot: repoRoot || REPO_ROOT });
|
|
409
|
+
reportSourceModeOf = sourceModeLookup(reportRouter.capabilities);
|
|
410
|
+
for (const providerId of missingProviders) {
|
|
411
|
+
probeByProvider.set(providerId, module.exports._probe(providerId, sourceRoot, reportSourceModeOf));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
405
414
|
}
|
|
406
415
|
|
|
407
|
-
|
|
408
|
-
|
|
416
|
+
let file ;
|
|
417
|
+
try {
|
|
418
|
+
file = updatePlan(projectRoot, taskId, slug, (disk ) => {
|
|
419
|
+
const current = requirePlanForUpdate(disk, taskId);
|
|
420
|
+
if (current.planId) {
|
|
421
|
+
const currentBinding = bindEvidence(current, items);
|
|
422
|
+
binding = currentBinding;
|
|
423
|
+
for (const need of current.needs) {
|
|
424
|
+
const bound = currentBinding.byNeed.get(need.intent) || [];
|
|
425
|
+
if (!Array.isArray(need.attempts)) need.attempts = [];
|
|
426
|
+
for (const attempt of need.attempts) {
|
|
427
|
+
if (attempt.recordedBy === 'derived') attempt.evidenceIds = [];
|
|
428
|
+
}
|
|
429
|
+
const groups = new Map ();
|
|
430
|
+
for (const item of bound) {
|
|
431
|
+
const absent = item.result === 'EMPTY';
|
|
432
|
+
const status = absent
|
|
433
|
+
? (item.freshness?.status === 'UNKNOWN' ? UNVERIFIED : EMPTY)
|
|
434
|
+
: RETRIEVED;
|
|
435
|
+
const providerKey = item.source?.provider ?? '__no_provider__';
|
|
436
|
+
const freshness = item.freshness?.status || 'UNKNOWN';
|
|
437
|
+
const key = `${providerKey}\u0000${status}\u0000${freshness}`;
|
|
438
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
439
|
+
const group = groups.get(key);
|
|
440
|
+
if (group) group.push(item);
|
|
441
|
+
}
|
|
442
|
+
const countedAttempts = need.attempts.filter((attempt ) =>
|
|
443
|
+
!(attempt.recordedBy === 'derived' && attempt.basis !== 'report-need'
|
|
444
|
+
&& (!Array.isArray(attempt.evidenceIds) || attempt.evidenceIds.length === 0)));
|
|
445
|
+
if (reached.has(need.intent) && countedAttempts.length === 0 && bound.length === 0) {
|
|
446
|
+
const provider = need.provider;
|
|
447
|
+
const indexState = provider ? (current.providers?.[provider]?.state || 'UNKNOWN') : NOT_APPLICABLE;
|
|
448
|
+
groups.set(`${provider ?? '__no_provider__'}\u0000${resultForEmptyAnswer(indexState)}\u0000${indexState}`, []);
|
|
449
|
+
}
|
|
450
|
+
for (const [key, group] of groups) {
|
|
451
|
+
const [providerKey, status, freshness] = key.split('\u0000');
|
|
452
|
+
const provider = providerKey === '__no_provider__' ? null : providerKey;
|
|
453
|
+
let attempt = need.attempts.find((entry ) => entry.recordedBy === 'derived'
|
|
454
|
+
&& entry.provider === provider && entry.status === status && entry.indexState === freshness);
|
|
455
|
+
if (attempt) {
|
|
456
|
+
attempt.evidenceIds = group.map((item ) => item.id);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (status === RETRIEVED && provider && !current.providers?.[provider] && probeByProvider.has(provider)) {
|
|
460
|
+
current.providers ||= {};
|
|
461
|
+
current.providers[provider] = probeByProvider.get(provider);
|
|
462
|
+
}
|
|
463
|
+
const providerState = provider ? current.providers?.[provider]?.state : null;
|
|
464
|
+
const indexState = status === RETRIEVED
|
|
465
|
+
? (providerState || 'UNKNOWN')
|
|
466
|
+
: (freshness || providerState || 'UNKNOWN');
|
|
467
|
+
const reason = status === RETRIEVED ? null
|
|
468
|
+
: indexState === 'STALE' ? 'index-stale'
|
|
469
|
+
: indexState === 'UNKNOWN' ? 'index-unverifiable' : 'empty-answer';
|
|
470
|
+
const n = need.attempts.reduce((max , entry ) => Math.max(max, entry.n || 0), 0) + 1;
|
|
471
|
+
need.attempts.push({
|
|
472
|
+
n, provider, capability: need.capability,
|
|
473
|
+
role: provider === null || provider === need.provider ? 'primary' : 'fallback',
|
|
474
|
+
status, reason, note: null, indexState,
|
|
475
|
+
attemptRef: current.attemptRef || null,
|
|
476
|
+
evidenceIds: group.map((item ) => item.id),
|
|
477
|
+
recordedAt: new Date().toISOString(), recordedBy: 'derived',
|
|
478
|
+
...(reached.has(need.intent) && group.length === 0 ? { basis: 'report-need' } : {}),
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
const outcome = deriveOutcome(need);
|
|
482
|
+
need.result = outcome.result;
|
|
483
|
+
need.resolvedBy = outcome.resolvedBy;
|
|
484
|
+
need.viaFallback = outcome.viaFallback;
|
|
485
|
+
need.evidenceIds = need.attempts.filter((entry ) => entry.status === RETRIEVED
|
|
486
|
+
&& !(entry.recordedBy === 'derived' && (!Array.isArray(entry.evidenceIds) || entry.evidenceIds.length === 0)))
|
|
487
|
+
.flatMap((entry ) => entry.evidenceIds || []);
|
|
488
|
+
}
|
|
489
|
+
record = current;
|
|
490
|
+
} else {
|
|
491
|
+
for (const need of current.needs) {
|
|
492
|
+
need.evidenceIds = evidenceIdsFor(need, items);
|
|
493
|
+
if (need.evidenceIds.length > 0) need.result = RETRIEVED;
|
|
494
|
+
else if (reached.has(need.intent)) need.result = resultForEmptyAnswer(freshnessOf(current, need));
|
|
495
|
+
else need.result = UNREACHED;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
current.reportedAt = new Date().toISOString();
|
|
499
|
+
record = current;
|
|
500
|
+
return current;
|
|
501
|
+
});
|
|
502
|
+
} catch (error ) {
|
|
503
|
+
return usageError('retrieval-plan', error.message, json, error);
|
|
504
|
+
}
|
|
409
505
|
|
|
410
506
|
// Derived views, computed at emit and never stored on a need — a need holds a provider id and
|
|
411
507
|
// nothing about that provider's index (plan D7).
|
|
412
|
-
const byResult = (result
|
|
413
|
-
const stale = record.needs.filter((need
|
|
508
|
+
const byResult = (result ) => record.needs.filter((need ) => need.result === result).map((need ) => need.intent);
|
|
509
|
+
const stale = record.needs.filter((need ) => freshnessOf(record, need) === 'STALE').map((need ) => need.intent);
|
|
414
510
|
const unreached = byResult(UNREACHED);
|
|
415
511
|
const unverified = byResult(UNVERIFIED);
|
|
416
512
|
const incomplete = unreached.length > 0 || unverified.length > 0;
|
|
@@ -430,6 +526,18 @@ function reportPlan({ projectRoot, taskId, reached, json, slug }: any) {
|
|
|
430
526
|
unreached,
|
|
431
527
|
unverified,
|
|
432
528
|
stale,
|
|
529
|
+
...(record.planId ? {
|
|
530
|
+
planId: record.planId,
|
|
531
|
+
binding: record.binding,
|
|
532
|
+
unbound: {
|
|
533
|
+
count: binding.unbound.length,
|
|
534
|
+
ids: binding.unbound.slice(0, 5).map((item ) => item.id),
|
|
535
|
+
causes: binding.unbound.reduce((counts , item ) => {
|
|
536
|
+
counts[item.cause] = (counts[item.cause] || 0) + 1;
|
|
537
|
+
return counts;
|
|
538
|
+
}, {}),
|
|
539
|
+
},
|
|
540
|
+
} : {}),
|
|
433
541
|
}, null, 2));
|
|
434
542
|
return finishRuntime(incomplete ? 1 : 0);
|
|
435
543
|
}
|
|
@@ -447,8 +555,21 @@ function reportPlan({ projectRoot, taskId, reached, json, slug }: any) {
|
|
|
447
555
|
+ freshness.padEnd(16)
|
|
448
556
|
+ need.result + (freshness === 'STALE' ? ' (stale index)' : ''),
|
|
449
557
|
);
|
|
558
|
+
if (record.planId) {
|
|
559
|
+
const attempts = need.attempts || [];
|
|
560
|
+
if (attempts.length > 1 || (attempts.length === 1 && attempts[0].status !== RETRIEVED)) {
|
|
561
|
+
for (const attempt of attempts) {
|
|
562
|
+
console.log(` Attempt #${attempt.n} ${attempt.role} ${attempt.provider} — ${attempt.status}`
|
|
563
|
+
+ (attempt.reason ? ` (${attempt.reason})` : '')
|
|
564
|
+
+ (attempt.evidenceIds?.length ? ` [${attempt.evidenceIds.join(', ')}]` : ''));
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
450
568
|
}
|
|
451
569
|
printProviderIndexes(record);
|
|
570
|
+
if (record.planId && binding.unbound.length > 0) {
|
|
571
|
+
console.log(`${binding.unbound.length} item(s) not bound to a declared need are not counted; re-record them with --need <intent>.`);
|
|
572
|
+
}
|
|
452
573
|
console.log('─'.repeat(78));
|
|
453
574
|
// NFR-004: the skip is stated as prominently as the failure, on its own line and named.
|
|
454
575
|
if (unreached.length > 0) {
|
|
@@ -464,9 +585,209 @@ function reportPlan({ projectRoot, taskId, reached, json, slug }: any) {
|
|
|
464
585
|
return finishRuntime(incomplete ? 1 : 0);
|
|
465
586
|
}
|
|
466
587
|
|
|
588
|
+
/**
|
|
589
|
+
* `--action attempt` — record an attempt against a declared need (IC-012, IC-013).
|
|
590
|
+
*
|
|
591
|
+
* Rules:
|
|
592
|
+
* - The action appends one attempt to the need, under the plan lock (IC-020).
|
|
593
|
+
* - `indexState` is re-probed for `--provider` with `freshnessEntry` at record time.
|
|
594
|
+
* - `--status EMPTY` is mapped through `resultForEmptyAnswer(indexState)`, so an EMPTY
|
|
595
|
+
* from an UNKNOWN index is stored as UNVERIFIED with reason `index-unverifiable`.
|
|
596
|
+
* - The action writes no evidence and never changes declared fields.
|
|
597
|
+
* - Exits 0 when written, 2 on any refusal.
|
|
598
|
+
*
|
|
599
|
+
* @returns {number} exit code
|
|
600
|
+
*/
|
|
601
|
+
function attemptPlan({
|
|
602
|
+
router,
|
|
603
|
+
projectRoot,
|
|
604
|
+
taskId,
|
|
605
|
+
intents,
|
|
606
|
+
provider,
|
|
607
|
+
attemptStatus,
|
|
608
|
+
reason,
|
|
609
|
+
note,
|
|
610
|
+
json,
|
|
611
|
+
slug,
|
|
612
|
+
sourceRoot,
|
|
613
|
+
} ) {
|
|
614
|
+
if (intents.length !== 1) {
|
|
615
|
+
return usageError(
|
|
616
|
+
'retrieval-plan',
|
|
617
|
+
`--need takes one intent on retrieval-plan --action attempt, got ${intents.length}. Record one write per need.`,
|
|
618
|
+
json,
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
const intent = intents[0];
|
|
622
|
+
|
|
623
|
+
let record ;
|
|
624
|
+
try {
|
|
625
|
+
record = readPlan(projectRoot, taskId, slug);
|
|
626
|
+
} catch (error ) {
|
|
627
|
+
return usageError('retrieval-plan', error.message, json, error);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const resolved = resolveNeed(record, intent, taskId);
|
|
631
|
+
if (!resolved.ok) {
|
|
632
|
+
return usageError('retrieval-plan', resolved.message, json);
|
|
633
|
+
}
|
|
634
|
+
const declaredNeed = resolved.need;
|
|
635
|
+
|
|
636
|
+
if (attemptStatus === undefined || attemptStatus === null || attemptStatus === '') {
|
|
637
|
+
return usageError('retrieval-plan', '--status is required: EMPTY, UNREACHED or UNVERIFIED', json);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (attemptStatus === 'RETRIEVED') {
|
|
641
|
+
return usageError(
|
|
642
|
+
'retrieval-plan',
|
|
643
|
+
`RETRIEVED is never recorded by hand: it is derived from evidence bound to the need. Record what was found with: doflow evidence --task-id ${taskId} --action add --need ${intent} ...`,
|
|
644
|
+
json,
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (attemptStatus !== 'EMPTY' && attemptStatus !== 'UNREACHED' && attemptStatus !== 'UNVERIFIED') {
|
|
649
|
+
return usageError(
|
|
650
|
+
'retrieval-plan',
|
|
651
|
+
`unknown --status '${attemptStatus}'. Valid here: EMPTY, UNREACHED, UNVERIFIED. A provider that could not be asked is UNREACHED with --reason provider-unavailable or policy-bypass.`,
|
|
652
|
+
json,
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const REASON_CODES_LIST = ATTEMPT_REASON_CODES.join(', ');
|
|
657
|
+
|
|
658
|
+
if (reason === undefined || reason === null || reason === '') {
|
|
659
|
+
return usageError(
|
|
660
|
+
'retrieval-plan',
|
|
661
|
+
`--status ${attemptStatus} needs --reason <code>: a lookup that returned no locator must say why. Codes: ${REASON_CODES_LIST}.`,
|
|
662
|
+
json,
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (!ATTEMPT_REASON_CODES.includes(reason)) {
|
|
667
|
+
return usageError(
|
|
668
|
+
'retrieval-plan',
|
|
669
|
+
`unknown --reason '${reason}'. Codes: ${REASON_CODES_LIST}.`,
|
|
670
|
+
json,
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const allowedReasons = {
|
|
675
|
+
EMPTY: ['empty-answer', 'index-stale', 'index-unverifiable'],
|
|
676
|
+
UNREACHED: ['provider-unavailable', 'policy-bypass', 'provider-error', 'timeout'],
|
|
677
|
+
UNVERIFIED: ['index-unverifiable'],
|
|
678
|
+
};
|
|
679
|
+
if (!allowedReasons[attemptStatus].includes(reason)) {
|
|
680
|
+
return usageError('retrieval-plan', `--reason '${reason}' is not allowed for --status ${attemptStatus}. Allowed: ${allowedReasons[attemptStatus].join(', ')}.`, json);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const capEntry = router.capabilities ? router.capabilities[declaredNeed.capability] : null;
|
|
684
|
+
const registeredProviders = capEntry && Array.isArray(capEntry.providers)
|
|
685
|
+
? capEntry.providers.map((p ) => p.id).filter(Boolean)
|
|
686
|
+
: [];
|
|
687
|
+
const regList = registeredProviders.join(', ');
|
|
688
|
+
|
|
689
|
+
if (provider === undefined || provider === null || String(provider).trim() === '') {
|
|
690
|
+
return usageError(
|
|
691
|
+
'retrieval-plan',
|
|
692
|
+
`--provider is required: an attempt is one provider's try at one need. Providers registered under ${declaredNeed.capability}: ${regList}.`,
|
|
693
|
+
json,
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (!registeredProviders.includes(provider)) {
|
|
698
|
+
return usageError(
|
|
699
|
+
'retrieval-plan',
|
|
700
|
+
`provider '${provider}' is not registered under capability '${declaredNeed.capability}' (need '${intent}'). Registered: ${regList}. A fallback is another provider of the same capability, for example native.rg.`,
|
|
701
|
+
json,
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (note !== undefined && note !== null && String(note).length > 160) {
|
|
706
|
+
return usageError(
|
|
707
|
+
'retrieval-plan',
|
|
708
|
+
`--note is at most 160 characters (got ${String(note).length}): it is a hint for a reader, and the runtime never reads it.`,
|
|
709
|
+
json,
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const sourceModeOf = sourceModeLookup(router.capabilities);
|
|
714
|
+
const probe = freshnessEntry(provider, sourceRoot, sourceModeOf);
|
|
715
|
+
const indexState = probe.state;
|
|
716
|
+
|
|
717
|
+
let finalStatus = attemptStatus;
|
|
718
|
+
let finalReason = reason;
|
|
719
|
+
|
|
720
|
+
if (attemptStatus === 'EMPTY') {
|
|
721
|
+
finalStatus = resultForEmptyAnswer(indexState);
|
|
722
|
+
if (finalStatus === 'UNVERIFIED') {
|
|
723
|
+
finalReason = 'index-unverifiable';
|
|
724
|
+
} else if (indexState === 'STALE') {
|
|
725
|
+
finalReason = 'index-stale';
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const activeRef = activeAttemptRef(projectRoot, taskId, slug);
|
|
730
|
+
let savedAttempt = null;
|
|
731
|
+
let file ;
|
|
732
|
+
try {
|
|
733
|
+
file = updatePlan(projectRoot, taskId, slug, (disk ) => {
|
|
734
|
+
const current = requirePlanForUpdate(disk, taskId);
|
|
735
|
+
const targetNeed = current.needs ? current.needs.find((n ) => n.intent === intent) : null;
|
|
736
|
+
if (!targetNeed) {
|
|
737
|
+
throw new Error(`need '${intent}' missing from disk plan during update`);
|
|
738
|
+
}
|
|
739
|
+
if (!Array.isArray(targetNeed.attempts)) {
|
|
740
|
+
targetNeed.attempts = [];
|
|
741
|
+
}
|
|
742
|
+
const n = targetNeed.attempts.length + 1;
|
|
743
|
+
const role = provider === targetNeed.provider ? 'primary' : 'fallback';
|
|
744
|
+
const attempt = {
|
|
745
|
+
n,
|
|
746
|
+
provider,
|
|
747
|
+
capability: targetNeed.capability,
|
|
748
|
+
role,
|
|
749
|
+
status: finalStatus,
|
|
750
|
+
reason: finalReason,
|
|
751
|
+
note: note !== undefined && note !== null ? String(note) : null,
|
|
752
|
+
indexState,
|
|
753
|
+
attemptRef: activeRef,
|
|
754
|
+
evidenceIds: [],
|
|
755
|
+
recordedAt: new Date().toISOString(),
|
|
756
|
+
recordedBy: 'caller',
|
|
757
|
+
};
|
|
758
|
+
targetNeed.attempts.push(attempt);
|
|
759
|
+
savedAttempt = attempt;
|
|
760
|
+
return current;
|
|
761
|
+
});
|
|
762
|
+
} catch (error ) {
|
|
763
|
+
return usageError('retrieval-plan', error.message, json, error);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (json) {
|
|
767
|
+
console.log(JSON.stringify({
|
|
768
|
+
action: 'attempt',
|
|
769
|
+
taskId,
|
|
770
|
+
intent,
|
|
771
|
+
stateFile: file,
|
|
772
|
+
attempt: savedAttempt,
|
|
773
|
+
}, null, 2));
|
|
774
|
+
return finishRuntime(0);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
console.log(`\nDoFlow Retrieval Attempt recorded [Task: ${taskId}]:`);
|
|
778
|
+
console.log('═'.repeat(78));
|
|
779
|
+
console.log(`Need: ${intent} Attempt: #${savedAttempt.n} Role: ${savedAttempt.role}`);
|
|
780
|
+
console.log(`Provider: ${savedAttempt.provider} Status: ${savedAttempt.status} Reason: ${savedAttempt.reason}`);
|
|
781
|
+
if (savedAttempt.note) console.log(`Note: ${savedAttempt.note}`);
|
|
782
|
+
console.log('─'.repeat(78));
|
|
783
|
+
console.log(`Recorded attempt #${savedAttempt.n} to ${file}`);
|
|
784
|
+
console.log('═'.repeat(78) + '\n');
|
|
785
|
+
return finishRuntime(0);
|
|
786
|
+
}
|
|
787
|
+
|
|
467
788
|
/** The provider index block both actions print: state, what was located, and the remedy. */
|
|
468
|
-
function printProviderIndexes(record
|
|
469
|
-
const entries = Object.entries
|
|
789
|
+
function printProviderIndexes(record ) {
|
|
790
|
+
const entries = Object.entries (record.providers || {});
|
|
470
791
|
if (entries.length === 0) return;
|
|
471
792
|
console.log('─'.repeat(78));
|
|
472
793
|
console.log('Provider indexes (probed once each, at declare time):');
|
|
@@ -481,9 +802,9 @@ function printProviderIndexes(record: any) {
|
|
|
481
802
|
// ── the verb ──────────────────────────────────────────────────────────────────────────────────
|
|
482
803
|
|
|
483
804
|
/** `--need a,b --need c` and `--need a --need b` are the same list. */
|
|
484
|
-
function parseIntents(raw
|
|
805
|
+
function parseIntents(raw ) {
|
|
485
806
|
const list = Array.isArray(raw) ? raw : (raw === undefined || raw === null ? [] : [raw]);
|
|
486
|
-
const out
|
|
807
|
+
const out = [];
|
|
487
808
|
for (const entry of list) {
|
|
488
809
|
for (const token of String(entry).split(',')) {
|
|
489
810
|
const trimmed = token.trim();
|
|
@@ -504,18 +825,19 @@ function parseIntents(raw: any) {
|
|
|
504
825
|
* @param {string} [options.stage] the stage id declaring the plan
|
|
505
826
|
* @param {boolean} [options.json=false]
|
|
506
827
|
* @param {string} [options.repoRoot] the DoFlow package root, holding the capability registries
|
|
507
|
-
* @param {string} [options.stateRoot] the project the task belongs to; its state
|
|
828
|
+
* @param {string} [options.stateRoot] the project the task belongs to; its state
|
|
829
|
+
* @param {string} [options.sourceRoot] the checkout whose source and index freshness are measured
|
|
508
830
|
* @returns {number} exit code
|
|
509
831
|
*/
|
|
510
|
-
function handleRetrievalPlanCommand(options
|
|
511
|
-
const { taskId, action = 'report', need, stage, json = false, repoRoot, stateRoot, slug } = options;
|
|
832
|
+
function handleRetrievalPlanCommand(options = {}) {
|
|
833
|
+
const { taskId, action = 'report', need, stage, provider, attemptStatus, reason, note, json = false, repoRoot, stateRoot, sourceRoot: requestedSourceRoot, slug } = options;
|
|
512
834
|
|
|
513
835
|
// NFR-001, at the boundary where a caller's input first becomes a record. `bin/doflow.js` already
|
|
514
836
|
// refuses every score-shaped flag name on the command line; this is the same rule applied to the
|
|
515
837
|
// option object, so a second input surface cannot arrive later without inheriting the refusal.
|
|
516
838
|
try {
|
|
517
839
|
assertNoScoreFields(options, '');
|
|
518
|
-
} catch (error
|
|
840
|
+
} catch (error ) {
|
|
519
841
|
return usageError('retrieval-plan', error.message, json, error);
|
|
520
842
|
}
|
|
521
843
|
|
|
@@ -523,18 +845,19 @@ function handleRetrievalPlanCommand(options: any = {}) {
|
|
|
523
845
|
// accepts it as an alias for its own default rather than reporting a usage error on an argument
|
|
524
846
|
// the caller never typed — `verify` and `claim` already do exactly this.
|
|
525
847
|
const resolvedAction = action === 'status' ? 'report' : action;
|
|
526
|
-
if (resolvedAction !== 'declare' && resolvedAction !== 'report') {
|
|
527
|
-
return usageError('retrieval-plan', `unknown --action '${action}'. Valid: declare, report (default)`, json);
|
|
848
|
+
if (resolvedAction !== 'declare' && resolvedAction !== 'report' && resolvedAction !== 'attempt') {
|
|
849
|
+
return usageError('retrieval-plan', `unknown --action '${action}'. Valid: declare, report (default), attempt`, json);
|
|
528
850
|
}
|
|
529
851
|
|
|
530
852
|
// The project the task belongs to owns both the state and the index whose age is being measured.
|
|
531
853
|
// The package root owns the capability registries, and is a different directory in every install.
|
|
532
854
|
const projectRoot = stateRoot || process.cwd();
|
|
855
|
+
const sourceRoot = requestedSourceRoot || projectRoot;
|
|
533
856
|
const packageRoot = repoRoot || REPO_ROOT;
|
|
534
857
|
|
|
535
858
|
try {
|
|
536
859
|
assertSafeTaskId(taskId);
|
|
537
|
-
} catch (error
|
|
860
|
+
} catch (error ) {
|
|
538
861
|
return usageError('retrieval-plan', error.message, json, error);
|
|
539
862
|
}
|
|
540
863
|
|
|
@@ -552,13 +875,35 @@ function handleRetrievalPlanCommand(options: any = {}) {
|
|
|
552
875
|
let router;
|
|
553
876
|
try {
|
|
554
877
|
router = new CapabilityRouter({ repoRoot: packageRoot });
|
|
555
|
-
} catch (error
|
|
878
|
+
} catch (error ) {
|
|
879
|
+
return usageError('retrieval-plan', error.message, json, error);
|
|
880
|
+
}
|
|
881
|
+
return declarePlan({ router, projectRoot, sourceRoot, taskId, stage, intents, json, slug });
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (resolvedAction === 'attempt') {
|
|
885
|
+
let router;
|
|
886
|
+
try {
|
|
887
|
+
router = new CapabilityRouter({ repoRoot: packageRoot });
|
|
888
|
+
} catch (error ) {
|
|
556
889
|
return usageError('retrieval-plan', error.message, json, error);
|
|
557
890
|
}
|
|
558
|
-
return
|
|
891
|
+
return attemptPlan({
|
|
892
|
+
router,
|
|
893
|
+
projectRoot,
|
|
894
|
+
sourceRoot,
|
|
895
|
+
taskId,
|
|
896
|
+
intents,
|
|
897
|
+
provider,
|
|
898
|
+
attemptStatus,
|
|
899
|
+
reason,
|
|
900
|
+
note,
|
|
901
|
+
json,
|
|
902
|
+
slug,
|
|
903
|
+
});
|
|
559
904
|
}
|
|
560
905
|
|
|
561
|
-
return reportPlan({ projectRoot, taskId, reached: new Set(intents), json, slug });
|
|
906
|
+
return reportPlan({ projectRoot, sourceRoot, taskId, reached: new Set(intents), json, slug, repoRoot: packageRoot });
|
|
562
907
|
}
|
|
563
908
|
|
|
564
909
|
module.exports = {
|
|
@@ -575,4 +920,8 @@ module.exports = {
|
|
|
575
920
|
NOT_APPLICABLE,
|
|
576
921
|
FRESHNESS_STATES,
|
|
577
922
|
EMPTY_ANSWER_BY_FRESHNESS,
|
|
923
|
+
ATTEMPT_REASON_CODES,
|
|
924
|
+
applyReport: (disk , taskId ) => requirePlanForUpdate(disk, taskId),
|
|
925
|
+
applyAttempt: (disk , taskId ) => requirePlanForUpdate(disk, taskId),
|
|
926
|
+
_probe: (providerId , root , sourceModeOf ) => freshnessEntry(providerId, root, sourceModeOf),
|
|
578
927
|
};
|