@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
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
* @returns {ShadowVerdict|null} `null` when `presentScopes` carries no usable scope name — there is
|
|
64
64
|
* no asset to resolve a verdict for. Otherwise a `ShadowVerdict`.
|
|
65
65
|
*/
|
|
66
|
-
function resolveShadowVerdict(presentScopes
|
|
66
|
+
function resolveShadowVerdict(presentScopes , precedence ) {
|
|
67
67
|
const scopes = Array.isArray(presentScopes)
|
|
68
|
-
? [...new Set(presentScopes.filter((scope
|
|
68
|
+
? [...new Set(presentScopes.filter((scope ) => typeof scope === 'string' && scope !== ''))]
|
|
69
69
|
: [];
|
|
70
70
|
if (scopes.length === 0) return null;
|
|
71
71
|
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
43
|
const os = require('node:os');
|
|
44
|
-
const { registryLifecycleView, LIFECYCLE_HARNESSES } = require('../../lifecycle/view.
|
|
45
|
-
const { readLock } = require('../../state/lockfile.
|
|
46
|
-
const { stateRoot, readLedger } = require('../../state/index.
|
|
47
|
-
const { recordedMcpSelections } = require('../../install/mcp.
|
|
48
|
-
const { readInstallManifest } = require('../../install/manifest.
|
|
44
|
+
const { registryLifecycleView, LIFECYCLE_HARNESSES } = require('../../lifecycle/view.js');
|
|
45
|
+
const { readLock } = require('../../state/lockfile.js');
|
|
46
|
+
const { stateRoot, readLedger } = require('../../state/index.js');
|
|
47
|
+
const { recordedMcpSelections } = require('../../install/mcp.js');
|
|
48
|
+
const { readInstallManifest } = require('../../install/manifest.js');
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* @typedef {Object} ScopeSnapshot The `SCOPE_SNAPSHOT` entity of the feature's data model.
|
|
@@ -68,7 +68,7 @@ const { readInstallManifest } = require('../../install/manifest.ts');
|
|
|
68
68
|
*/
|
|
69
69
|
|
|
70
70
|
/** @returns {ScopeSnapshot} */
|
|
71
|
-
function snapshot(scope
|
|
71
|
+
function snapshot(scope , view , targets ) {
|
|
72
72
|
const { ledger } = view;
|
|
73
73
|
const wanted = new Set(targets);
|
|
74
74
|
return {
|
|
@@ -77,7 +77,7 @@ function snapshot(scope: any, view: any, targets: any): any {
|
|
|
77
77
|
stateRoot: view.stateRoot,
|
|
78
78
|
recorded: ledger.resources.length > 0 || Object.keys(ledger.targets).length > 0,
|
|
79
79
|
targets,
|
|
80
|
-
resources: ledger.resources.filter((resource
|
|
80
|
+
resources: ledger.resources.filter((resource ) => wanted.has(resource?.harness)),
|
|
81
81
|
ledger,
|
|
82
82
|
plan: view.plan,
|
|
83
83
|
};
|
|
@@ -104,7 +104,7 @@ function snapshot(scope: any, view: any, targets: any): any {
|
|
|
104
104
|
* computed from a selection nobody can read, so a currency judgement derived from it would be
|
|
105
105
|
* fiction. `reconcile` fails on the same document.
|
|
106
106
|
*/
|
|
107
|
-
function readScopes({ registry, repoRoot, projectRoot, targets = LIFECYCLE_HARNESSES, locks }
|
|
107
|
+
function readScopes({ registry, repoRoot, projectRoot, targets = LIFECYCLE_HARNESSES, locks } ) {
|
|
108
108
|
const wanted = [...new Set(targets)];
|
|
109
109
|
const resolved = locks ?? {
|
|
110
110
|
// The global lock root is the home directory, the same root design R7 keeps out of this
|
|
@@ -112,7 +112,7 @@ function readScopes({ registry, repoRoot, projectRoot, targets = LIFECYCLE_HARNE
|
|
|
112
112
|
global: readLock({ scope: 'global' }),
|
|
113
113
|
project: readLock({ scope: 'project', projectRoot }),
|
|
114
114
|
};
|
|
115
|
-
const read = (scope
|
|
115
|
+
const read = (scope ) => {
|
|
116
116
|
// Each scope's own ledger, at the root the view itself reads it from.
|
|
117
117
|
const ledger = readLedger(scope === 'global'
|
|
118
118
|
? stateRoot({ scope: 'global', homeDir: os.homedir() }) : stateRoot({ scope: 'project', projectRoot }));
|
|
@@ -88,8 +88,8 @@ const WHOLESALE_ASSETS = Object.freeze({
|
|
|
88
88
|
|
|
89
89
|
/** Whether this harness loads this asset's native directory wholesale. `Array.isArray` also guards
|
|
90
90
|
* a harness id that collides with an `Object.prototype` key. */
|
|
91
|
-
function loadsWholesale(harness
|
|
92
|
-
const assets = (WHOLESALE_ASSETS
|
|
91
|
+
function loadsWholesale(harness , assetId ) {
|
|
92
|
+
const assets = (WHOLESALE_ASSETS )[harness];
|
|
93
93
|
return Array.isArray(assets) && assets.includes(assetId);
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -98,7 +98,7 @@ function loadsWholesale(harness: any, assetId: any): boolean {
|
|
|
98
98
|
* subdirectory holding managed assets is recognised as structure, not as a foreign entry); only
|
|
99
99
|
* resources whose harness loads them wholesale contribute a `wholesale` target, and only a
|
|
100
100
|
* directory with at least one of those is ever read. */
|
|
101
|
-
function groupByParentDir(resources
|
|
101
|
+
function groupByParentDir(resources ) {
|
|
102
102
|
const byDir = new Map();
|
|
103
103
|
const managedDirs = new Set();
|
|
104
104
|
for (const resource of resources ?? []) {
|
|
@@ -119,10 +119,10 @@ function groupByParentDir(resources: any): any {
|
|
|
119
119
|
/** Whether a directory entry is part of the managed tree's own structure — it is, or contains at
|
|
120
120
|
* some depth, the parent directory of a managed resource. Scans `managedDirs`, which is bounded by
|
|
121
121
|
* the number of managed resources rather than by directory size (R6). */
|
|
122
|
-
function holdsManagedAsset(candidatePath
|
|
122
|
+
function holdsManagedAsset(candidatePath , managedDirs ) {
|
|
123
123
|
const prefix = candidatePath + path.sep;
|
|
124
124
|
for (const dir of managedDirs) {
|
|
125
|
-
if (dir === candidatePath || (dir
|
|
125
|
+
if (dir === candidatePath || (dir ).startsWith(prefix)) return true;
|
|
126
126
|
}
|
|
127
127
|
return false;
|
|
128
128
|
}
|
|
@@ -152,13 +152,13 @@ function holdsManagedAsset(candidatePath: any, managedDirs: any): boolean {
|
|
|
152
152
|
* beside eight reported siblings invited the reading that eight more had been hidden. IC-002's
|
|
153
153
|
* final field is dropped with it (task F.3); see `index.js#buildInventoryReport`.
|
|
154
154
|
*/
|
|
155
|
-
function inspectSiblings({ resources, fsImpl = fs }
|
|
155
|
+
function inspectSiblings({ resources, fsImpl = fs } = {}) {
|
|
156
156
|
const { byDir, managedDirs } = groupByParentDir(resources);
|
|
157
|
-
const candidatesByTarget
|
|
157
|
+
const candidatesByTarget = {};
|
|
158
158
|
let directoriesInspected = 0;
|
|
159
159
|
|
|
160
160
|
for (const [dir, { basenames, wholesale }] of byDir) {
|
|
161
|
-
if ((wholesale
|
|
161
|
+
if ((wholesale ).length === 0) continue; // name-resolved: never read, not even to count.
|
|
162
162
|
|
|
163
163
|
let entries;
|
|
164
164
|
try {
|
|
@@ -168,12 +168,12 @@ function inspectSiblings({ resources, fsImpl = fs }: any = {}): any {
|
|
|
168
168
|
}
|
|
169
169
|
directoriesInspected += 1;
|
|
170
170
|
|
|
171
|
-
const unmanaged
|
|
171
|
+
const unmanaged = [];
|
|
172
172
|
for (const entry of entries) {
|
|
173
|
-
const name = (entry
|
|
174
|
-
if ((basenames
|
|
175
|
-
const entryPath = path.join(dir
|
|
176
|
-
const isDirectory = (entry
|
|
173
|
+
const name = (entry ).name;
|
|
174
|
+
if ((basenames ).has(name)) continue; // the managed file itself.
|
|
175
|
+
const entryPath = path.join(dir , name);
|
|
176
|
+
const isDirectory = (entry ).isDirectory?.() === true;
|
|
177
177
|
if (isDirectory && holdsManagedAsset(entryPath, managedDirs)) continue; // managed structure.
|
|
178
178
|
unmanaged.push({ name, path: entryPath, parentDir: dir, kind: isDirectory ? 'directory' : 'file' });
|
|
179
179
|
}
|
|
@@ -181,8 +181,8 @@ function inspectSiblings({ resources, fsImpl = fs }: any = {}): any {
|
|
|
181
181
|
|
|
182
182
|
// Attributed per managed target rather than shared, so each candidate names the harness and
|
|
183
183
|
// asset whose wholesale load is the reason it is operative at all.
|
|
184
|
-
for (const { target, harness, assetId } of (wholesale
|
|
185
|
-
candidatesByTarget[target] = unmanaged.map((found
|
|
184
|
+
for (const { target, harness, assetId } of (wholesale )) {
|
|
185
|
+
candidatesByTarget[target] = unmanaged.map((found ) => ({ ...found, harness, assetId }));
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
@@ -15,10 +15,10 @@ const HARD_MAX_CHUNK_CHARS = 1200;
|
|
|
15
15
|
/** Split raw markdown into chunks: {id, path, section, text}. Ids are stable path+ordinal pairs,
|
|
16
16
|
* not content hashes — the index manifest owns content addressing, and a chunk whose text changed
|
|
17
17
|
* should keep its identity so diffs are legible. */
|
|
18
|
-
function chunkMarkdown(rawText
|
|
18
|
+
function chunkMarkdown(rawText , { path: filePath } = {}) {
|
|
19
19
|
const { body, title } = stripFrontmatter(String(rawText ?? ''));
|
|
20
20
|
const sections = splitSections(body);
|
|
21
|
-
const chunks
|
|
21
|
+
const chunks = [];
|
|
22
22
|
let ordinal = 0;
|
|
23
23
|
for (const section of sections) {
|
|
24
24
|
for (const piece of packSection(section.text)) {
|
|
@@ -35,7 +35,7 @@ function chunkMarkdown(rawText: any, { path: filePath }: { path?: string } = {})
|
|
|
35
35
|
return chunks;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function stripFrontmatter(text
|
|
38
|
+
function stripFrontmatter(text ) {
|
|
39
39
|
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
|
|
40
40
|
if (!match) return { body: text, title: null };
|
|
41
41
|
const meta = match[1];
|
|
@@ -44,10 +44,10 @@ function stripFrontmatter(text: string): { body: string; title: string | null }
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/** Heading-bounded sections; a leading H1 is metadata (the title), not a section boundary. */
|
|
47
|
-
function splitSections(body
|
|
47
|
+
function splitSections(body ) {
|
|
48
48
|
const lines = body.split(/\r?\n/);
|
|
49
|
-
const sections
|
|
50
|
-
let current
|
|
49
|
+
const sections = [];
|
|
50
|
+
let current = { heading: null, lines: [] };
|
|
51
51
|
const flush = () => {
|
|
52
52
|
if (current.lines.some((l) => l.trim() !== '')) {
|
|
53
53
|
sections.push({ heading: current.heading, text: current.lines.join('\n') });
|
|
@@ -68,9 +68,9 @@ function splitSections(body: string): Array<{ heading: string | null; text: stri
|
|
|
68
68
|
|
|
69
69
|
/** Group a section's blocks (paragraphs, lists, fenced code) into chunks near the target size.
|
|
70
70
|
* A block larger than the hard max (a long code fence) stands alone rather than being cut. */
|
|
71
|
-
function packSection(text
|
|
71
|
+
function packSection(text ) {
|
|
72
72
|
const blocks = splitBlocks(text).filter((b) => b.trim() !== '');
|
|
73
|
-
const packed
|
|
73
|
+
const packed = [];
|
|
74
74
|
let buffer = '';
|
|
75
75
|
for (const block of blocks) {
|
|
76
76
|
if (block.length > HARD_MAX_CHUNK_CHARS) {
|
|
@@ -91,11 +91,11 @@ function packSection(text: string): string[] {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/** Split into blocks at blank lines, keeping fenced code blocks intact across their blank lines. */
|
|
94
|
-
function splitBlocks(text
|
|
94
|
+
function splitBlocks(text ) {
|
|
95
95
|
const lines = text.split(/\r?\n/);
|
|
96
|
-
const blocks
|
|
97
|
-
let buffer
|
|
98
|
-
let fence
|
|
96
|
+
const blocks = [];
|
|
97
|
+
let buffer = [];
|
|
98
|
+
let fence = null;
|
|
99
99
|
for (const line of lines) {
|
|
100
100
|
const opening = /^\s*(```|~~~)/.exec(line);
|
|
101
101
|
if (fence) {
|
|
@@ -11,9 +11,9 @@ const path = require('node:path');
|
|
|
11
11
|
* @param {{ k?: number }} [options]
|
|
12
12
|
* @returns {Array<{ id: string, rrfScore: number }>}
|
|
13
13
|
*/
|
|
14
|
-
function reciprocalRankFusion(rankedLists
|
|
15
|
-
const scores = new Map
|
|
16
|
-
const items = new Map
|
|
14
|
+
function reciprocalRankFusion(rankedLists , { k = 60 } = {}) {
|
|
15
|
+
const scores = new Map ();
|
|
16
|
+
const items = new Map ();
|
|
17
17
|
|
|
18
18
|
for (const list of rankedLists) {
|
|
19
19
|
if (!Array.isArray(list)) continue;
|
|
@@ -32,7 +32,7 @@ function reciprocalRankFusion(rankedLists: any[][], { k = 60 }: { k?: number } =
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const fused
|
|
35
|
+
const fused = [];
|
|
36
36
|
for (const [id, rrfScore] of scores.entries()) {
|
|
37
37
|
fused.push({
|
|
38
38
|
...items.get(id),
|
|
@@ -41,7 +41,7 @@ function reciprocalRankFusion(rankedLists: any[][], { k = 60 }: { k?: number } =
|
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
fused.sort((a
|
|
44
|
+
fused.sort((a , b ) => b.rrfScore - a.rrfScore || (a.id < b.id ? -1 : 1));
|
|
45
45
|
return fused;
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -51,7 +51,7 @@ function reciprocalRankFusion(rankedLists: any[][], { k = 60 }: { k?: number } =
|
|
|
51
51
|
* @param {string} [projectRoot]
|
|
52
52
|
* @returns {{ file: string, line?: number, endLine?: number }}
|
|
53
53
|
*/
|
|
54
|
-
function normalizeLocator(raw
|
|
54
|
+
function normalizeLocator(raw , projectRoot = process.cwd()) {
|
|
55
55
|
if (!raw) return { file: '' };
|
|
56
56
|
if (typeof raw === 'string') {
|
|
57
57
|
const match = raw.match(/^([^:]+)(?::(\d+))?(?::(\d+))?$/);
|
|
@@ -97,22 +97,22 @@ function fuseHybridResults({
|
|
|
97
97
|
projectRoot = process.cwd(),
|
|
98
98
|
maxSnippetLines = 50,
|
|
99
99
|
fsImpl = fs,
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const normLex = (Array.isArray(lexicalResults) ? lexicalResults : []).map((item
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
= {}) {
|
|
109
|
+
const normLex = (Array.isArray(lexicalResults) ? lexicalResults : []).map((item , idx ) => {
|
|
110
110
|
const loc = normalizeLocator(item.locator || item.path || item.file, projectRoot);
|
|
111
111
|
const id = `${loc.file}:${loc.line || 1}`;
|
|
112
112
|
return { ...item, id, locator: loc, sourceList: 'lexical', originalRank: idx };
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
-
const normSem = (Array.isArray(semanticResults) ? semanticResults : []).map((item
|
|
115
|
+
const normSem = (Array.isArray(semanticResults) ? semanticResults : []).map((item , idx ) => {
|
|
116
116
|
const loc = normalizeLocator(item.locator || item.path || item.file, projectRoot);
|
|
117
117
|
const id = `${loc.file}:${loc.line || 1}`;
|
|
118
118
|
return { ...item, id, locator: loc, sourceList: 'semantic', originalRank: idx };
|
|
@@ -11,23 +11,23 @@
|
|
|
11
11
|
const fs = require('node:fs');
|
|
12
12
|
const path = require('node:path');
|
|
13
13
|
const crypto = require('node:crypto');
|
|
14
|
-
const { chunkMarkdown } = require('./chunker.
|
|
14
|
+
const { chunkMarkdown } = require('./chunker.js');
|
|
15
15
|
|
|
16
16
|
const INDEX_VERSION = 1;
|
|
17
17
|
const MANIFEST_FILE = 'manifest.json';
|
|
18
18
|
const CHUNKS_FILE = 'chunks.jsonl';
|
|
19
19
|
|
|
20
|
-
function sha256(buffer
|
|
20
|
+
function sha256(buffer ) {
|
|
21
21
|
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** Every .md file under the corpus root, as sorted relative paths (deterministic builds). */
|
|
25
|
-
function listMarkdownFiles(corpusDir
|
|
26
|
-
const out
|
|
27
|
-
const walk = (dir
|
|
28
|
-
let entries
|
|
25
|
+
function listMarkdownFiles(corpusDir , { fsImpl = fs } = {}) {
|
|
26
|
+
const out = [];
|
|
27
|
+
const walk = (dir ) => {
|
|
28
|
+
let entries ;
|
|
29
29
|
try { entries = fsImpl.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
30
|
-
entries.sort((a
|
|
30
|
+
entries.sort((a , b ) => (a.name < b.name ? -1 : 1));
|
|
31
31
|
for (const entry of entries) {
|
|
32
32
|
const abs = path.join(dir, entry.name);
|
|
33
33
|
if (entry.isDirectory()) walk(abs);
|
|
@@ -38,22 +38,22 @@ function listMarkdownFiles(corpusDir: string, { fsImpl = fs }: { fsImpl?: any }
|
|
|
38
38
|
return out.sort((a, b) => (a < b ? -1 : 1));
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function readManifest(indexDir
|
|
41
|
+
function readManifest(indexDir , { fsImpl = fs } = {}) {
|
|
42
42
|
try {
|
|
43
43
|
return JSON.parse(fsImpl.readFileSync(path.join(indexDir, MANIFEST_FILE), 'utf8'));
|
|
44
44
|
} catch { return null; }
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function loadChunksByPath(indexDir
|
|
48
|
-
const byPath = new Map
|
|
49
|
-
let raw
|
|
47
|
+
function loadChunksByPath(indexDir , { fsImpl = fs } = {}) {
|
|
48
|
+
const byPath = new Map ();
|
|
49
|
+
let raw ;
|
|
50
50
|
try { raw = fsImpl.readFileSync(path.join(indexDir, CHUNKS_FILE), 'utf8'); } catch { return byPath; }
|
|
51
51
|
for (const line of raw.split('\n')) {
|
|
52
52
|
if (!line.trim()) continue;
|
|
53
|
-
let chunk
|
|
53
|
+
let chunk ;
|
|
54
54
|
try { chunk = JSON.parse(line); } catch { continue; }
|
|
55
55
|
if (!byPath.has(chunk.path)) byPath.set(chunk.path, []);
|
|
56
|
-
byPath.get(chunk.path)
|
|
56
|
+
byPath.get(chunk.path) .push(chunk);
|
|
57
57
|
}
|
|
58
58
|
return byPath;
|
|
59
59
|
}
|
|
@@ -61,22 +61,22 @@ function loadChunksByPath(indexDir: string, { fsImpl = fs }: { fsImpl?: any } =
|
|
|
61
61
|
/** Build (or incrementally refresh) the index. Files whose digest already sits in the manifest
|
|
62
62
|
* reuse their stored chunks byte-for-byte; only changed or new files are re-chunked. Returns
|
|
63
63
|
* {files, chunks, rebuilt[], reusedCount}. */
|
|
64
|
-
function buildIndex({ corpusDir, indexDir, fsImpl = fs }
|
|
64
|
+
function buildIndex({ corpusDir, indexDir, fsImpl = fs } = {} ) {
|
|
65
65
|
const relPaths = listMarkdownFiles(corpusDir, { fsImpl });
|
|
66
66
|
const previous = readManifest(indexDir, { fsImpl });
|
|
67
67
|
const previousFiles = previous?.files ?? {};
|
|
68
68
|
const reusableChunks = loadChunksByPath(indexDir, { fsImpl });
|
|
69
69
|
|
|
70
|
-
const files
|
|
71
|
-
const allChunks
|
|
72
|
-
const rebuilt
|
|
70
|
+
const files = {};
|
|
71
|
+
const allChunks = [];
|
|
72
|
+
const rebuilt = [];
|
|
73
73
|
let reusedCount = 0;
|
|
74
74
|
for (const rel of relPaths) {
|
|
75
75
|
const content = fsImpl.readFileSync(path.join(corpusDir, rel), 'utf8');
|
|
76
76
|
const digest = sha256(Buffer.from(content, 'utf8'));
|
|
77
77
|
const prior = previousFiles[rel];
|
|
78
78
|
if (prior && prior.digest === digest && reusableChunks.has(rel)) {
|
|
79
|
-
const chunks = reusableChunks.get(rel)
|
|
79
|
+
const chunks = reusableChunks.get(rel) ;
|
|
80
80
|
files[rel] = { digest, chunks: chunks.length };
|
|
81
81
|
allChunks.push(...chunks);
|
|
82
82
|
reusedCount += 1;
|
|
@@ -89,7 +89,7 @@ function buildIndex({ corpusDir, indexDir, fsImpl = fs }: { corpusDir: string; i
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
fsImpl.mkdirSync(indexDir, { recursive: true });
|
|
92
|
-
const body = allChunks.map((chunk
|
|
92
|
+
const body = allChunks.map((chunk ) => JSON.stringify(chunk)).join('\n') + (allChunks.length ? '\n' : '');
|
|
93
93
|
const tmpChunks = `${path.join(indexDir, CHUNKS_FILE)}.${process.pid}.tmp`;
|
|
94
94
|
fsImpl.writeFileSync(tmpChunks, body, 'utf8');
|
|
95
95
|
fsImpl.renameSync(tmpChunks, path.join(indexDir, CHUNKS_FILE));
|
|
@@ -108,11 +108,11 @@ function buildIndex({ corpusDir, indexDir, fsImpl = fs }: { corpusDir: string; i
|
|
|
108
108
|
|
|
109
109
|
/** Load the index for querying. Returns null when absent/malformed; staleness is the caller's
|
|
110
110
|
* policy decision (see ensureFreshIndex), never a silent assumption here. */
|
|
111
|
-
function loadIndex({ indexDir, fsImpl = fs }
|
|
111
|
+
function loadIndex({ indexDir, fsImpl = fs } = {} ) {
|
|
112
112
|
const manifest = readManifest(indexDir, { fsImpl });
|
|
113
113
|
if (!manifest || manifest.version !== INDEX_VERSION) return null;
|
|
114
|
-
const chunks
|
|
115
|
-
let raw
|
|
114
|
+
const chunks = [];
|
|
115
|
+
let raw ;
|
|
116
116
|
try { raw = fsImpl.readFileSync(path.join(indexDir, CHUNKS_FILE), 'utf8'); } catch { return null; }
|
|
117
117
|
for (const line of raw.split('\n')) {
|
|
118
118
|
if (!line.trim()) continue;
|
|
@@ -123,7 +123,7 @@ function loadIndex({ indexDir, fsImpl = fs }: { indexDir: string; fsImpl?: any }
|
|
|
123
123
|
|
|
124
124
|
/** True when every corpus file's current digest matches the manifest. Missing extra corpus files
|
|
125
125
|
* (present on disk, absent from the manifest) also count as stale — they are not yet searchable. */
|
|
126
|
-
function isFresh({ corpusDir, indexDir, fsImpl = fs }
|
|
126
|
+
function isFresh({ corpusDir, indexDir, fsImpl = fs } = {} ) {
|
|
127
127
|
const manifest = readManifest(indexDir, { fsImpl });
|
|
128
128
|
if (!manifest) return false;
|
|
129
129
|
const relPaths = listMarkdownFiles(corpusDir, { fsImpl });
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
const fs = require('node:fs');
|
|
12
12
|
const path = require('node:path');
|
|
13
|
-
const { buildIndex, loadIndex, isFresh } = require('./index-store.
|
|
14
|
-
const { finishRuntime, usageError, captureCaught } = require('../cli-result.
|
|
15
|
-
const { resolveRunsLocation } = require('../trace/ledger.
|
|
13
|
+
const { buildIndex, loadIndex, isFresh } = require('./index-store.js');
|
|
14
|
+
const { finishRuntime, usageError, captureCaught } = require('../cli-result.js');
|
|
15
|
+
const { resolveRunsLocation } = require('../trace/ledger.js');
|
|
16
16
|
|
|
17
17
|
const BM25_K1 = 1.5;
|
|
18
18
|
const BM25_B = 0.75;
|
|
@@ -22,7 +22,7 @@ const STOPWORDS = new Set([
|
|
|
22
22
|
'do', 'does', 'did', 'how', 'what', 'when', 'which', 'who', 'why', 'not', 'no', 'yes', 'can',
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
|
-
function tokenize(text
|
|
25
|
+
function tokenize(text ) {
|
|
26
26
|
return String(text ?? '')
|
|
27
27
|
.toLowerCase()
|
|
28
28
|
.split(/[^a-z0-9]+/)
|
|
@@ -31,24 +31,24 @@ function tokenize(text: any): string[] {
|
|
|
31
31
|
|
|
32
32
|
/** Deterministic BM25 (Okapi) over preloaded chunks. Corpus sizes here are thousands of chunks at
|
|
33
33
|
* most, so scoring at query time from plain arrays beats maintaining incremental postings. */
|
|
34
|
-
function bm25Rank(queryText
|
|
34
|
+
function bm25Rank(queryText , chunks , { k = 5 } = {}) {
|
|
35
35
|
const queryTerms = [...new Set(tokenize(queryText))];
|
|
36
36
|
if (!queryTerms.length || !chunks.length) return [];
|
|
37
|
-
const docTokens = chunks.map((chunk
|
|
38
|
-
const avgLen = docTokens.reduce((sum
|
|
37
|
+
const docTokens = chunks.map((chunk ) => tokenize(`${chunk.title ?? ''} ${chunk.text}`));
|
|
38
|
+
const avgLen = docTokens.reduce((sum , tokens ) => sum + tokens.length, 0) / docTokens.length || 1;
|
|
39
39
|
|
|
40
|
-
const df = new Map
|
|
40
|
+
const df = new Map ();
|
|
41
41
|
for (const term of queryTerms) {
|
|
42
42
|
let count = 0;
|
|
43
43
|
for (const tokens of docTokens) if (tokens.includes(term)) count += 1;
|
|
44
44
|
df.set(term, count);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const scored
|
|
47
|
+
const scored = [];
|
|
48
48
|
for (let i = 0; i < chunks.length; i += 1) {
|
|
49
49
|
const tokens = docTokens[i];
|
|
50
50
|
if (!tokens.length) continue;
|
|
51
|
-
const tf = new Map
|
|
51
|
+
const tf = new Map ();
|
|
52
52
|
for (const token of tokens) tf.set(token, (tf.get(token) ?? 0) + 1);
|
|
53
53
|
let score = 0;
|
|
54
54
|
for (const term of queryTerms) {
|
|
@@ -67,17 +67,17 @@ function bm25Rank(queryText: string, chunks: any[], { k = 5 }: { k?: number } =
|
|
|
67
67
|
/** Registry-derived document graph: every `@file.md` import inside a corpus document is an edge.
|
|
68
68
|
* Guidance's own path-anchor rule makes these resolvable against the corpus root, which turns the
|
|
69
69
|
* content itself into the knowledge graph — no LLM extraction, no external store. */
|
|
70
|
-
function buildImportGraph(corpusDir
|
|
71
|
-
const edges = new Map
|
|
70
|
+
function buildImportGraph(corpusDir , { fsImpl = fs } = {}) {
|
|
71
|
+
const edges = new Map ();
|
|
72
72
|
try { fsImpl.readdirSync(corpusDir); } catch { return edges; }
|
|
73
|
-
const walk = (dir
|
|
73
|
+
const walk = (dir , rel ) => {
|
|
74
74
|
for (const entry of fsImpl.readdirSync(dir, { withFileTypes: true })) {
|
|
75
75
|
const abs = path.join(dir, entry.name);
|
|
76
76
|
const relPath = rel ? `${rel}/${entry.name}` : entry.name;
|
|
77
77
|
if (entry.isDirectory()) { walk(abs, relPath); continue; }
|
|
78
78
|
if (!entry.name.toLowerCase().endsWith('.md')) continue;
|
|
79
79
|
const text = (() => { try { return fsImpl.readFileSync(abs, 'utf8'); } catch { return ''; } })();
|
|
80
|
-
const targets = new Set
|
|
80
|
+
const targets = new Set ();
|
|
81
81
|
for (const match of text.matchAll(/(?:^|\s)@([^\s`]+\.md)\b/g)) {
|
|
82
82
|
const target = path.posix.normalize(match[1].replace(/^\.\//, ''));
|
|
83
83
|
if (target.startsWith('..') || path.isAbsolute(target)) continue;
|
|
@@ -93,17 +93,17 @@ function buildImportGraph(corpusDir: string, { fsImpl = fs }: { fsImpl?: any } =
|
|
|
93
93
|
/** Expand BM25 hits along import edges (1 hop): a linked document is "what to read next" for the
|
|
94
94
|
* question that surfaced its anchor. Graph results are capped separately and never displace
|
|
95
95
|
* lexical hits — they extend the answer instead of reranking it. */
|
|
96
|
-
function expandViaGraph(hits
|
|
97
|
-
const hitPaths = new Set(hits.map((h
|
|
98
|
-
const seen = new Set
|
|
99
|
-
const extras
|
|
96
|
+
function expandViaGraph(hits , chunks , graph , { maxExtra = 3 } = {}) {
|
|
97
|
+
const hitPaths = new Set(hits.map((h ) => h.path));
|
|
98
|
+
const seen = new Set ();
|
|
99
|
+
const extras = [];
|
|
100
100
|
for (const hit of hits) {
|
|
101
101
|
for (const target of graph.get(hit.path) ?? []) {
|
|
102
102
|
if (hitPaths.has(target) || seen.has(target)) continue;
|
|
103
103
|
seen.add(target);
|
|
104
|
-
const linkedChunks = chunks.filter((c
|
|
104
|
+
const linkedChunks = chunks.filter((c ) => c.path === target);
|
|
105
105
|
if (!linkedChunks.length) continue;
|
|
106
|
-
const best = linkedChunks.reduce((a
|
|
106
|
+
const best = linkedChunks.reduce((a , b ) => (b.text.length > a.text.length ? b : a));
|
|
107
107
|
extras.push({
|
|
108
108
|
path: best.path,
|
|
109
109
|
title: best.title,
|
|
@@ -119,18 +119,18 @@ function expandViaGraph(hits: any[], chunks: any[], graph: Map<string, string[]>
|
|
|
119
119
|
|
|
120
120
|
/** Ensure the on-disk index matches the corpus, rebuilding only what changed, then rank.
|
|
121
121
|
* Returns {results, rebuilt}. A missing corpus is the caller's usage problem, thrown loudly. */
|
|
122
|
-
function searchGuidance({ corpusDir, indexDir, query, k = 5, fsImpl = fs }
|
|
122
|
+
function searchGuidance({ corpusDir, indexDir, query, k = 5, fsImpl = fs } = {} ) {
|
|
123
123
|
if (!fsImpl.existsSync(corpusDir)) {
|
|
124
124
|
throw new Error(`No guidance tree found at '${corpusDir}' — run \`doflow install\` first`);
|
|
125
125
|
}
|
|
126
|
-
let rebuiltFiles
|
|
126
|
+
let rebuiltFiles = null;
|
|
127
127
|
if (!isFresh({ corpusDir, indexDir, fsImpl })) {
|
|
128
128
|
const build = buildIndex({ corpusDir, indexDir, fsImpl });
|
|
129
129
|
rebuiltFiles = build.rebuilt;
|
|
130
130
|
}
|
|
131
131
|
const loaded = loadIndex({ indexDir, fsImpl });
|
|
132
132
|
if (!loaded) throw new Error(`Index at '${indexDir}' is unreadable after rebuild`);
|
|
133
|
-
const ranked = bm25Rank(query, loaded.chunks, { k }).map((hit
|
|
133
|
+
const ranked = bm25Rank(query, loaded.chunks, { k }).map((hit ) => ({
|
|
134
134
|
path: hit.path,
|
|
135
135
|
title: hit.title,
|
|
136
136
|
score: Number(hit.score.toFixed(4)),
|
|
@@ -145,7 +145,7 @@ function searchGuidance({ corpusDir, indexDir, query, k = 5, fsImpl = fs }: { co
|
|
|
145
145
|
* code from this install, knowledge from the project being worked on. The `.doflow` root is
|
|
146
146
|
* $DOFLOW_CONFIG_DIR, else the nearest one above `stateRoot` that holds a guidance tree, else the
|
|
147
147
|
* home install; the index is kept beside the corpus chosen. */
|
|
148
|
-
function handleRetrieveCommand({ query, top, json = false, stateRoot }
|
|
148
|
+
function handleRetrieveCommand({ query, top, json = false, stateRoot } = {}) {
|
|
149
149
|
const { configDir } = resolveRunsLocation({ start: stateRoot, containing: 'guidance' });
|
|
150
150
|
const corpusDir = path.join(configDir, 'guidance');
|
|
151
151
|
const indexDir = path.join(configDir, 'index', 'guidance');
|
|
@@ -161,13 +161,13 @@ function handleRetrieveCommand({ query, top, json = false, stateRoot }: { query?
|
|
|
161
161
|
} else if (!results.length) {
|
|
162
162
|
console.log('[OK] No matching guidance found.');
|
|
163
163
|
} else {
|
|
164
|
-
results.forEach((result
|
|
164
|
+
results.forEach((result , i ) => {
|
|
165
165
|
console.log(`${i + 1}. [${result.score}] ${result.path}${result.title ? ` — ${result.title}` : ''}`);
|
|
166
166
|
console.log(` ${result.snippet}`);
|
|
167
167
|
});
|
|
168
168
|
}
|
|
169
169
|
return finishRuntime(0);
|
|
170
|
-
} catch (error
|
|
170
|
+
} catch (error ) {
|
|
171
171
|
captureCaught(error, 'retrieve', 1);
|
|
172
172
|
console.error(`[ERROR] retrieve: ${error.message}`);
|
|
173
173
|
return finishRuntime(1);
|
|
@@ -178,7 +178,7 @@ const {
|
|
|
178
178
|
reciprocalRankFusion,
|
|
179
179
|
normalizeLocator,
|
|
180
180
|
fuseHybridResults,
|
|
181
|
-
} = require('./hybrid.
|
|
181
|
+
} = require('./hybrid.js');
|
|
182
182
|
|
|
183
183
|
module.exports = {
|
|
184
184
|
bm25Rank,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('node:fs');
|
|
6
6
|
const path = require('node:path');
|
|
7
|
-
const { finishRuntime, usageError } = require('./cli-result.
|
|
7
|
+
const { finishRuntime, usageError } = require('./cli-result.js');
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Reports DoFlow's own process vocabulary appearing in files that ship to people who never used
|
|
@@ -44,7 +44,7 @@ const DEFAULT_EXCLUDED_SEGMENTS = ['agent-docs'];
|
|
|
44
44
|
// here as an exported Set that nothing read, including no test: a second place to keep the list
|
|
45
45
|
// correct, with nothing to notice when the two disagreed.
|
|
46
46
|
|
|
47
|
-
function isExcluded(relPath
|
|
47
|
+
function isExcluded(relPath , excludedSegments ) {
|
|
48
48
|
const segments = relPath.split(path.sep);
|
|
49
49
|
return excludedSegments.some((seg) => segments.includes(seg));
|
|
50
50
|
}
|
|
@@ -58,7 +58,7 @@ function isExcluded(relPath: string, excludedSegments: string[]): boolean {
|
|
|
58
58
|
*
|
|
59
59
|
* @returns {{relative: string, content: string}|{relative: string, reason: string}}
|
|
60
60
|
*/
|
|
61
|
-
function readScannable(target
|
|
61
|
+
function readScannable(target , root , excluded , impl ) {
|
|
62
62
|
const absolute = path.isAbsolute(target) ? target : path.join(root, target);
|
|
63
63
|
const relative = path.relative(root, absolute);
|
|
64
64
|
|
|
@@ -77,8 +77,8 @@ function readScannable(target: string, root: string, excluded: string[], impl: a
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/** Every pattern match in one file's lines, as finding records. */
|
|
80
|
-
function findLeaksInLines(lines
|
|
81
|
-
const findings
|
|
80
|
+
function findLeaksInLines(lines , relative ) {
|
|
81
|
+
const findings = [];
|
|
82
82
|
for (let i = 0; i < lines.length; i += 1) {
|
|
83
83
|
for (const rule of LEAK_PATTERNS) {
|
|
84
84
|
const match = lines[i].match(rule.pattern);
|
|
@@ -104,14 +104,14 @@ function findLeaksInLines(lines: string[], relative: string): any[] {
|
|
|
104
104
|
* @param {Object} [options.fsImpl] injectable fs, for tests
|
|
105
105
|
* @returns {{findings: Array, scanned: string[], unscanned: Array}}
|
|
106
106
|
*/
|
|
107
|
-
function scanPaths({ paths: targets, repoRoot, excludedSegments, fsImpl }
|
|
107
|
+
function scanPaths({ paths: targets, repoRoot, excludedSegments, fsImpl } = {}) {
|
|
108
108
|
const impl = fsImpl || fs;
|
|
109
109
|
const root = repoRoot || process.cwd();
|
|
110
110
|
const excluded = excludedSegments || DEFAULT_EXCLUDED_SEGMENTS;
|
|
111
111
|
|
|
112
|
-
const findings
|
|
113
|
-
const scanned
|
|
114
|
-
const unscanned
|
|
112
|
+
const findings = [];
|
|
113
|
+
const scanned = [];
|
|
114
|
+
const unscanned = [];
|
|
115
115
|
|
|
116
116
|
for (const target of targets || []) {
|
|
117
117
|
if (typeof target !== 'string' || target.trim() === '') continue;
|
|
@@ -123,7 +123,7 @@ function scanPaths({ paths: targets, repoRoot, excludedSegments, fsImpl }: any =
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
scanned.push(read.relative);
|
|
126
|
-
findings.push(...findLeaksInLines((read.content
|
|
126
|
+
findings.push(...findLeaksInLines((read.content ).split('\n'), read.relative));
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
return { findings, scanned, unscanned };
|
|
@@ -140,14 +140,14 @@ function scanPaths({ paths: targets, repoRoot, excludedSegments, fsImpl }: any =
|
|
|
140
140
|
* @param {string} [options.repoRoot]
|
|
141
141
|
* @returns {number} exit code
|
|
142
142
|
*/
|
|
143
|
-
function handleLeakScanCommand({ paths: targets, exclude, json = false, repoRoot }
|
|
143
|
+
function handleLeakScanCommand({ paths: targets, exclude, json = false, repoRoot } = {}) {
|
|
144
144
|
if (!Array.isArray(targets) || targets.length === 0) {
|
|
145
145
|
return usageError('leak-scan', '--path is required (repeatable): the files to scan', json);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
// `--exclude` extends the built-in artifact-directory exclusion rather than replacing it: a
|
|
149
149
|
// caller narrowing the scan should not be able to accidentally widen it back over agent-docs/.
|
|
150
|
-
const extra = Array.isArray(exclude) ? exclude.filter((e
|
|
150
|
+
const extra = Array.isArray(exclude) ? exclude.filter((e ) => typeof e === 'string' && e.trim() !== '') : [];
|
|
151
151
|
const excludedSegments = [...DEFAULT_EXCLUDED_SEGMENTS, ...extra];
|
|
152
152
|
|
|
153
153
|
const result = scanPaths({ paths: targets, repoRoot, excludedSegments });
|