@khoavu882/doflow 2.0.0-beta.3 → 2.0.0-beta.4
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/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/registry/storage.json +87 -83
- package/core/registry/verification.json +1 -1
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
- package/core/shared/skills/do-execute-plan/references/scaffold.md +2 -2
- package/package.json +4 -2
- package/src/adapters/antigravity/{index.ts → index.js} +60 -60
- package/src/adapters/claude/{index.ts → index.js} +48 -48
- 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} +23 -23
- package/src/adapters/codex/{index.ts → index.js} +98 -98
- 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} +70 -70
- package/src/adapters/gemini/{hooks.ts → hooks.js} +24 -24
- package/src/adapters/gemini/{index.ts → index.js} +41 -41
- 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} +34 -34
- 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/{index.ts → index.js} +30 -30
- 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} +24 -24
- 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} +19 -19
- package/src/cli/{index.ts → index.js} +26 -26
- package/src/cli/{runtime-commands.ts → runtime-commands.js} +48 -48
- 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/{index.ts → index.js} +140 -140
- 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} +60 -60
- 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/{capability-router.ts → capability-router.js} +39 -39
- package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
- package/src/runtime/{claims.ts → claims.js} +30 -30
- package/src/runtime/{cli-result.ts → cli-result.js} +4 -4
- package/src/runtime/{cli.ts → cli.js} +37 -37
- package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
- package/src/runtime/{context-pack.ts → context-pack.js} +20 -20
- package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
- package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +25 -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} +15 -15
- 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} +35 -35
- 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} +40 -40
- package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +19 -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/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} +61 -61
- package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
- package/src/runtime/{task-scope.ts → task-scope.js} +13 -13
- package/src/runtime/{task-state.ts → task-state.js} +15 -15
- 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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('node:fs');
|
|
6
6
|
const path = require('node:path');
|
|
7
|
-
const { resolveActiveFeature } = require('./feature-resolve.
|
|
7
|
+
const { resolveActiveFeature } = require('./feature-resolve.js');
|
|
8
8
|
|
|
9
9
|
// Which feature a task record belongs to (feature 045, IC-001/IC-002).
|
|
10
10
|
//
|
|
@@ -19,7 +19,7 @@ const SAFE_SLUG = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
|
19
19
|
const SLUG_RULE = 'use letters, digits, dot, underscore or dash, start with a letter or digit, and no ".."';
|
|
20
20
|
|
|
21
21
|
/** The one shape check for a feature slug (IC-009); do-paths.sh applies the same rule in bash. */
|
|
22
|
-
function isSafeSlug(slug
|
|
22
|
+
function isSafeSlug(slug ) {
|
|
23
23
|
return typeof slug === 'string' && SAFE_SLUG.test(slug) && !slug.includes('..');
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -29,31 +29,31 @@ function isSafeSlug(slug: any): boolean {
|
|
|
29
29
|
* @param {*} slug
|
|
30
30
|
* @returns {{error: 'invalid-slug', message: string, hint: string}|null}
|
|
31
31
|
*/
|
|
32
|
-
function invalidSlugRefusal(slug
|
|
32
|
+
function invalidSlugRefusal(slug ) {
|
|
33
33
|
if (typeof slug !== 'string' || slug === '' || isSafeSlug(slug)) return null;
|
|
34
34
|
return { error: 'invalid-slug', message: `slug "${slug}" is not a valid feature slug: ${SLUG_RULE}`, hint: SLUG_RULE };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/** (projectRoot, slug) -> { slug, hasRegister } | { reason }. One resolver spawn per key per process. */
|
|
38
|
-
const featureCache = new Map
|
|
38
|
+
const featureCache = new Map ();
|
|
39
39
|
|
|
40
40
|
/** A slug the CLI named explicitly (`--slug`), applied to every store call in this process that was
|
|
41
41
|
* not given its own. The dispatcher sets it once so the verbs whose handlers build their ledger
|
|
42
42
|
* internally (readiness, evidence) route the same way as the ones that take `slug` directly. */
|
|
43
|
-
let defaultSlug
|
|
43
|
+
let defaultSlug = null;
|
|
44
44
|
|
|
45
|
-
function setDefaultSlug(slug
|
|
45
|
+
function setDefaultSlug(slug ) {
|
|
46
46
|
defaultSlug = typeof slug === 'string' && slug !== '' ? slug : null;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function clearTaskScopeCache()
|
|
49
|
+
function clearTaskScopeCache() {
|
|
50
50
|
featureCache.clear();
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/** The resolver reports the git top level, which is the project root or one of its ancestors, and
|
|
54
54
|
* the feature folders live directly under it — so no `agent-docs/doflow` on the way up means there
|
|
55
55
|
* is nothing to resolve and the spawn is skipped. */
|
|
56
|
-
function hasFeatureFolderAbove(projectRoot
|
|
56
|
+
function hasFeatureFolderAbove(projectRoot ) {
|
|
57
57
|
let dir = path.resolve(projectRoot);
|
|
58
58
|
for (;;) {
|
|
59
59
|
try {
|
|
@@ -65,10 +65,10 @@ function hasFeatureFolderAbove(projectRoot: string): boolean {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function featureFor(projectRoot
|
|
68
|
+
function featureFor(projectRoot , slug ) {
|
|
69
69
|
const key = `${path.resolve(projectRoot)}\0${slug || ''}`;
|
|
70
70
|
if (featureCache.has(key)) return featureCache.get(key);
|
|
71
|
-
let value
|
|
71
|
+
let value = { reason: 'no-feature' };
|
|
72
72
|
try {
|
|
73
73
|
if (hasFeatureFolderAbove(projectRoot)) {
|
|
74
74
|
const found = resolveActiveFeature({ projectRoot, slug: slug || null });
|
|
@@ -90,7 +90,7 @@ function featureFor(projectRoot: string, slug: string | null | undefined): any {
|
|
|
90
90
|
* `reason` is `namespaced` when a namespace applies, else `no-feature`, `no-register` or
|
|
91
91
|
* `feature-level-id`.
|
|
92
92
|
*/
|
|
93
|
-
function resolveTaskScope({ projectRoot, taskId, slug = null }
|
|
93
|
+
function resolveTaskScope({ projectRoot, taskId, slug = null } ) {
|
|
94
94
|
const feature = featureFor(projectRoot, slug || defaultSlug);
|
|
95
95
|
if (feature.reason) return { namespace: null, slug: null, reason: feature.reason };
|
|
96
96
|
if (!feature.hasRegister) return { namespace: null, slug: feature.slug, reason: 'no-register' };
|
|
@@ -107,7 +107,7 @@ function resolveTaskScope({ projectRoot, taskId, slug = null }: { projectRoot: s
|
|
|
107
107
|
* @param {string} options.slug
|
|
108
108
|
* @returns {boolean}
|
|
109
109
|
*/
|
|
110
|
-
function slugNamesNoFeature({ projectRoot, slug }
|
|
110
|
+
function slugNamesNoFeature({ projectRoot, slug } ) {
|
|
111
111
|
const feature = featureFor(projectRoot, slug);
|
|
112
112
|
return Boolean(feature.reason) || feature.exists === false;
|
|
113
113
|
}
|
|
@@ -121,7 +121,7 @@ function slugNamesNoFeature({ projectRoot, slug }: { projectRoot: string; slug:
|
|
|
121
121
|
* @param {string|null} [options.slug]
|
|
122
122
|
* @returns {string}
|
|
123
123
|
*/
|
|
124
|
-
function taskStoreDir({ projectRoot, store, taskId, slug = null }
|
|
124
|
+
function taskStoreDir({ projectRoot, store, taskId, slug = null } ) {
|
|
125
125
|
const base = path.join(projectRoot, '.doflow', 'state', store);
|
|
126
126
|
const { namespace } = resolveTaskScope({ projectRoot, taskId, slug });
|
|
127
127
|
return namespace ? path.join(base, namespace) : base;
|
|
@@ -28,8 +28,8 @@ const { isDeepStrictEqual } = require('node:util');
|
|
|
28
28
|
|
|
29
29
|
/** Three-way merge against the reader's snapshot. Unchanged local records never overwrite a
|
|
30
30
|
* newer disk version; concurrent edits to the same record must be retried after an explicit read. */
|
|
31
|
-
function mergeRecords(local
|
|
32
|
-
const merged = new Map(disk.map((item
|
|
31
|
+
function mergeRecords(local , disk , baseline ) {
|
|
32
|
+
const merged = new Map(disk.map((item ) => [item.id, item]));
|
|
33
33
|
for (const item of local) {
|
|
34
34
|
const previous = baseline.get(item.id);
|
|
35
35
|
const current = merged.get(item.id);
|
|
@@ -48,7 +48,7 @@ const LOCK_RETRIES = 250;
|
|
|
48
48
|
const LOCK_WAIT_MS = 20;
|
|
49
49
|
|
|
50
50
|
/** Synchronous sleep without spinning: Atomics.wait on a throwaway buffer. */
|
|
51
|
-
function sleep(ms
|
|
51
|
+
function sleep(ms ) {
|
|
52
52
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -59,28 +59,28 @@ function sleep(ms: number): void {
|
|
|
59
59
|
* @param {string} file
|
|
60
60
|
* @returns {(() => void)|null}
|
|
61
61
|
*/
|
|
62
|
-
function tryAcquireLock(fsImpl
|
|
62
|
+
function tryAcquireLock(fsImpl , file ) {
|
|
63
63
|
const lockDir = `${file}.lock`;
|
|
64
64
|
const release = () => { try { fsImpl.rmdirSync(lockDir); } catch { /* released is released */ } };
|
|
65
65
|
try {
|
|
66
66
|
fsImpl.mkdirSync(lockDir);
|
|
67
67
|
return release;
|
|
68
|
-
} catch (error
|
|
68
|
+
} catch (error ) {
|
|
69
69
|
if (error.code !== 'EEXIST') throw error;
|
|
70
70
|
}
|
|
71
|
-
let lock
|
|
72
|
-
try { lock = fsImpl.statSync(lockDir); } catch (error
|
|
71
|
+
let lock ;
|
|
72
|
+
try { lock = fsImpl.statSync(lockDir); } catch (error ) {
|
|
73
73
|
if (error.code !== 'ENOENT') return null;
|
|
74
|
-
try { fsImpl.mkdirSync(lockDir); return release; } catch (retryError
|
|
74
|
+
try { fsImpl.mkdirSync(lockDir); return release; } catch (retryError ) {
|
|
75
75
|
if (retryError.code === 'EEXIST') return null;
|
|
76
76
|
throw retryError;
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
if (Date.now() - lock.mtimeMs <= LOCK_STALE_MS) return null;
|
|
80
|
-
try { fsImpl.rmdirSync(lockDir); } catch (error
|
|
80
|
+
try { fsImpl.rmdirSync(lockDir); } catch (error ) {
|
|
81
81
|
if (error.code !== 'ENOENT') return null;
|
|
82
82
|
}
|
|
83
|
-
try { fsImpl.mkdirSync(lockDir); return release; } catch (error
|
|
83
|
+
try { fsImpl.mkdirSync(lockDir); return release; } catch (error ) {
|
|
84
84
|
if (error.code === 'EEXIST') return null;
|
|
85
85
|
throw error;
|
|
86
86
|
}
|
|
@@ -93,7 +93,7 @@ function tryAcquireLock(fsImpl: any, file: string): (() => void) | null {
|
|
|
93
93
|
* @param {string} file
|
|
94
94
|
* @returns {() => void}
|
|
95
95
|
*/
|
|
96
|
-
function acquireLock(fsImpl
|
|
96
|
+
function acquireLock(fsImpl , file ) {
|
|
97
97
|
const lockDir = `${file}.lock`;
|
|
98
98
|
// The stale rule below is wall-clock, so the wait must be too: on a host where each attempt costs
|
|
99
99
|
// real time, counting attempts alone would outlive a live holder's staleness window and then
|
|
@@ -123,12 +123,12 @@ function acquireLock(fsImpl: any, file: string): () => void {
|
|
|
123
123
|
* @param {string} file
|
|
124
124
|
* @returns {Object|null} the parsed payload
|
|
125
125
|
*/
|
|
126
|
-
function readTaskState(fsImpl
|
|
126
|
+
function readTaskState(fsImpl , file ) {
|
|
127
127
|
if (!fsImpl.existsSync(file)) return null;
|
|
128
|
-
let data
|
|
128
|
+
let data ;
|
|
129
129
|
try {
|
|
130
130
|
data = JSON.parse(fsImpl.readFileSync(file, 'utf8'));
|
|
131
|
-
} catch (error
|
|
131
|
+
} catch (error ) {
|
|
132
132
|
throw new Error(`Failed to load state file '${file}': ${error.message}. The write path is atomic `
|
|
133
133
|
+ '(tmp + rename), so a torn file means outside interference; recover it from version control '
|
|
134
134
|
+ 'or move it aside and re-record the task\'s items.');
|
|
@@ -153,7 +153,7 @@ function readTaskState(fsImpl: any, file: string): any {
|
|
|
153
153
|
* that must inspect or set aside what is there under the same exclusion as the write
|
|
154
154
|
* @returns {string} the file path written
|
|
155
155
|
*/
|
|
156
|
-
function updateTaskState({ fsImpl, file, build, prepare }
|
|
156
|
+
function updateTaskState({ fsImpl, file, build, prepare } ) {
|
|
157
157
|
fsImpl.mkdirSync(path.dirname(file), { recursive: true });
|
|
158
158
|
const release = acquireLock(fsImpl, file);
|
|
159
159
|
try {
|
|
@@ -26,16 +26,16 @@
|
|
|
26
26
|
|
|
27
27
|
const fs = require('node:fs');
|
|
28
28
|
const path = require('node:path');
|
|
29
|
-
const { formatMs, finish } = require('./render.
|
|
29
|
+
const { formatMs, finish } = require('./render.js');
|
|
30
30
|
|
|
31
31
|
const ORCHESTRATION_DIRNAME = 'orchestration';
|
|
32
32
|
|
|
33
33
|
/** Mirrors resolveRunsLocation's shape, for the sibling state directory this reads. */
|
|
34
|
-
function orchestrationDir(root
|
|
34
|
+
function orchestrationDir(root ) {
|
|
35
35
|
return path.join(root, '.doflow', 'state', ORCHESTRATION_DIRNAME);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function parseAt(value
|
|
38
|
+
function parseAt(value ) {
|
|
39
39
|
const ms = Date.parse(value);
|
|
40
40
|
return Number.isNaN(ms) ? null : ms;
|
|
41
41
|
}
|
|
@@ -43,10 +43,10 @@ function parseAt(value: any): number | null {
|
|
|
43
43
|
/** Every record in a directory, plus the names of the ones that did not parse. A file that cannot be
|
|
44
44
|
* read is counted and named rather than skipped: a report that quietly drops what it could not read
|
|
45
45
|
* overstates its own coverage, which is the defect `malformedLines` already guards against for runs. */
|
|
46
|
-
function readRecords(dir
|
|
46
|
+
function readRecords(dir , fsImpl = fs) {
|
|
47
47
|
if (!fsImpl.existsSync(dir)) return { records: [], unreadable: [], exists: false };
|
|
48
|
-
const records
|
|
49
|
-
const unreadable
|
|
48
|
+
const records = [];
|
|
49
|
+
const unreadable = [];
|
|
50
50
|
for (const name of fsImpl.readdirSync(dir).sort()) {
|
|
51
51
|
if (!name.endsWith('.json')) continue;
|
|
52
52
|
try {
|
|
@@ -56,7 +56,7 @@ function readRecords(dir: string, fsImpl: any = fs): any {
|
|
|
56
56
|
continue;
|
|
57
57
|
}
|
|
58
58
|
records.push(parsed);
|
|
59
|
-
} catch (err
|
|
59
|
+
} catch (err ) {
|
|
60
60
|
unreadable.push({ file: name, reason: err.message });
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -65,11 +65,11 @@ function readRecords(dir: string, fsImpl: any = fs): any {
|
|
|
65
65
|
|
|
66
66
|
/** A stage row is "not work done" when the orchestrator backfilled it or imported it rather than
|
|
67
67
|
* executing it. Its duration measures when the record was written, so it is listed but never averaged. */
|
|
68
|
-
function isSynthetic(entry
|
|
68
|
+
function isSynthetic(entry ) {
|
|
69
69
|
return Boolean(entry.backfilled) || (entry.executionStatus && entry.executionStatus !== 'completed');
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function median(values
|
|
72
|
+
function median(values ) {
|
|
73
73
|
if (!values.length) return null;
|
|
74
74
|
const sorted = [...values].sort((a, b) => a - b);
|
|
75
75
|
const mid = Math.floor(sorted.length / 2);
|
|
@@ -77,11 +77,11 @@ function median(values: number[]): number | null {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/** One run's rows, walking history in recorded order with a cursor so no interval is counted twice. */
|
|
80
|
-
function runRows(record
|
|
80
|
+
function runRows(record ) {
|
|
81
81
|
const history = Array.isArray(record.history) ? record.history : [];
|
|
82
82
|
let cursor = parseAt(record.startedAt);
|
|
83
|
-
const stages
|
|
84
|
-
const gates
|
|
83
|
+
const stages = [];
|
|
84
|
+
const gates = [];
|
|
85
85
|
let reruns = 0;
|
|
86
86
|
|
|
87
87
|
for (const entry of history) {
|
|
@@ -131,13 +131,13 @@ function runRows(record: any): any {
|
|
|
131
131
|
/** Median elapsed per stage id across runs of one class. Synthetic rows are excluded here and still
|
|
132
132
|
* listed in their run: averaging the moment a file was written into the figure most likely to be
|
|
133
133
|
* quoted would corrupt it. */
|
|
134
|
-
function aggregateStages(runs
|
|
135
|
-
const perStage = new Map
|
|
134
|
+
function aggregateStages(runs ) {
|
|
135
|
+
const perStage = new Map ();
|
|
136
136
|
for (const run of runs) {
|
|
137
137
|
for (const row of run.stages) {
|
|
138
138
|
if (row.synthetic || row.elapsedMs === null || !row.stage) continue;
|
|
139
139
|
if (!perStage.has(row.stage)) perStage.set(row.stage, []);
|
|
140
|
-
perStage.get(row.stage)
|
|
140
|
+
perStage.get(row.stage) .push(row.elapsedMs);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
return [...perStage.entries()].map(([stage, values]) => ({
|
|
@@ -146,14 +146,14 @@ function aggregateStages(runs: any[]): any[] {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
/** Pure. Reads nothing and writes nothing; takes already-parsed records. */
|
|
149
|
-
function buildIndicators({ records = [], unreadable = [], exists = true } = {})
|
|
150
|
-
const runs = (records
|
|
151
|
-
const byClass = new Map
|
|
149
|
+
function buildIndicators({ records = [], unreadable = [], exists = true } = {}) {
|
|
150
|
+
const runs = (records ).map(runRows);
|
|
151
|
+
const byClass = new Map ();
|
|
152
152
|
|
|
153
153
|
for (const run of runs) {
|
|
154
154
|
const key = run.taskClass || '(unclassified)';
|
|
155
155
|
if (!byClass.has(key)) byClass.set(key, { taskClass: key, runs: [], stageAggregate: [] });
|
|
156
|
-
byClass.get(key)
|
|
156
|
+
byClass.get(key) .runs.push(run);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
for (const group of byClass.values()) group.stageAggregate = aggregateStages(group.runs);
|
|
@@ -176,7 +176,7 @@ function buildIndicators({ records = [], unreadable = [], exists = true } = {}):
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
/** One class block: its runs, each run's stage and gate rows, then the class median. */
|
|
179
|
-
function printClass(group
|
|
179
|
+
function printClass(group ) {
|
|
180
180
|
console.log('─'.repeat(78));
|
|
181
181
|
console.log(`CLASS: ${group.taskClass} (${group.runs.length} run(s))`);
|
|
182
182
|
for (const run of group.runs) {
|
|
@@ -195,7 +195,7 @@ function printClass(group: any): void {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
function handleIndicatorsCommand({ json = false, projectRoot = process.cwd() } = {})
|
|
198
|
+
function handleIndicatorsCommand({ json = false, projectRoot = process.cwd() } = {}) {
|
|
199
199
|
const dir = orchestrationDir(projectRoot);
|
|
200
200
|
const read = readRecords(dir);
|
|
201
201
|
const view = buildIndicators(read);
|
|
@@ -220,7 +220,7 @@ function handleIndicatorsCommand({ json = false, projectRoot = process.cwd() } =
|
|
|
220
220
|
|
|
221
221
|
console.log(`Runs: ${view.runCount} across ${view.byClass.length} task class(es)`);
|
|
222
222
|
if (view.unreadable.length) {
|
|
223
|
-
console.log(`Unreadable: ${view.unreadable.length} record(s) — ${view.unreadable.map((u
|
|
223
|
+
console.log(`Unreadable: ${view.unreadable.length} record(s) — ${view.unreadable.map((u ) => u.file).join(', ')}`);
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
for (const group of view.byClass) printClass(group);
|
|
@@ -12,7 +12,7 @@ const {
|
|
|
12
12
|
buildStats,
|
|
13
13
|
buildDiscover,
|
|
14
14
|
PROVIDER,
|
|
15
|
-
} = require('./views.
|
|
15
|
+
} = require('./views.js');
|
|
16
16
|
const {
|
|
17
17
|
shortTime,
|
|
18
18
|
formatMs,
|
|
@@ -20,7 +20,7 @@ const {
|
|
|
20
20
|
integrityNote,
|
|
21
21
|
printEmptyLedgerNotice,
|
|
22
22
|
DISCOVER_MARK,
|
|
23
|
-
} = require('./render.
|
|
23
|
+
} = require('./render.js');
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Task-run telemetry — the JavaScript port of
|
|
@@ -49,7 +49,7 @@ const {
|
|
|
49
49
|
* They price one specific model; a trace naming a different one gets a cost only if the caller
|
|
50
50
|
* supplies its rates, because inventing a price for an unpriced model is inventing a metric.
|
|
51
51
|
*/
|
|
52
|
-
const DEFAULT_TOKEN_RATES
|
|
52
|
+
const DEFAULT_TOKEN_RATES = Object.freeze({
|
|
53
53
|
inputPerToken: 0.000003,
|
|
54
54
|
outputPerToken: 0.000015,
|
|
55
55
|
});
|
|
@@ -60,20 +60,20 @@ const COST_DECIMALS = 6;
|
|
|
60
60
|
* @param {number} value
|
|
61
61
|
* @returns {number} rounded to six decimals, matching the Python's `round(x, 6)`
|
|
62
62
|
*/
|
|
63
|
-
function roundCost(value
|
|
63
|
+
function roundCost(value ) {
|
|
64
64
|
const factor = 10 ** COST_DECIMALS;
|
|
65
65
|
return Math.round(value * factor) / factor;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
class TaskRunTelemetry {
|
|
69
|
-
taskId
|
|
70
|
-
agent
|
|
71
|
-
model
|
|
72
|
-
rates
|
|
73
|
-
events
|
|
74
|
-
clock
|
|
75
|
-
startedAtMs
|
|
76
|
-
startedAt
|
|
69
|
+
taskId ;
|
|
70
|
+
agent ;
|
|
71
|
+
model ;
|
|
72
|
+
rates ;
|
|
73
|
+
events ;
|
|
74
|
+
clock ;
|
|
75
|
+
startedAtMs ;
|
|
76
|
+
startedAt ;
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* @param {string} taskId
|
|
@@ -84,7 +84,7 @@ class TaskRunTelemetry {
|
|
|
84
84
|
* @param {{inputPerToken: number, outputPerToken: number}|null} [options.rates]
|
|
85
85
|
* @param {Function} [options.clock] injection seam for tests; returns elapsed milliseconds
|
|
86
86
|
*/
|
|
87
|
-
constructor(taskId
|
|
87
|
+
constructor(taskId , options = {}) {
|
|
88
88
|
const opts = typeof options === 'string' ? { agent: options } : options;
|
|
89
89
|
this.taskId = taskId;
|
|
90
90
|
this.agent = opts.agent || 'codex';
|
|
@@ -105,7 +105,7 @@ class TaskRunTelemetry {
|
|
|
105
105
|
* @param {Object} [details={}]
|
|
106
106
|
* @returns {Object} the recorded event
|
|
107
107
|
*/
|
|
108
|
-
recordEvent(eventType
|
|
108
|
+
recordEvent(eventType , details = {}) {
|
|
109
109
|
const event = {
|
|
110
110
|
timestamp: new Date().toISOString(),
|
|
111
111
|
type: eventType,
|
|
@@ -119,7 +119,7 @@ class TaskRunTelemetry {
|
|
|
119
119
|
* Milliseconds elapsed since the recorder was constructed.
|
|
120
120
|
* @returns {number}
|
|
121
121
|
*/
|
|
122
|
-
elapsedMs()
|
|
122
|
+
elapsedMs() {
|
|
123
123
|
return Math.max(0, this.clock() - this.startedAtMs);
|
|
124
124
|
}
|
|
125
125
|
|
|
@@ -130,7 +130,7 @@ class TaskRunTelemetry {
|
|
|
130
130
|
* @param {number} [outputTokens=0]
|
|
131
131
|
* @returns {Object} trace record
|
|
132
132
|
*/
|
|
133
|
-
exportTrace(success
|
|
133
|
+
exportTrace(success , inputTokens = 0, outputTokens = 0) {
|
|
134
134
|
// Defect fix (2): cost is only reported when rates are known for this run. The default rates
|
|
135
135
|
// reproduce the Python's number exactly; a caller that clears them (`rates: null`) gets `null`
|
|
136
136
|
// rather than a confident figure derived from another model's price list.
|
|
@@ -180,7 +180,7 @@ const LEDGER_FILE_RE = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
|
180
180
|
* `wire` is the on-disk key: snake_case, matching the five fields `doflow-run` already writes, so
|
|
181
181
|
* one file never mixes two naming conventions.
|
|
182
182
|
*/
|
|
183
|
-
const RUN_FIELDS
|
|
183
|
+
const RUN_FIELDS = Object.freeze([
|
|
184
184
|
{ key: 'verb', wire: 'verb', type: 'token' },
|
|
185
185
|
{ key: 'kind', wire: 'kind', type: 'token' },
|
|
186
186
|
{ key: 'capability', wire: 'capability', type: 'token' },
|
|
@@ -215,7 +215,7 @@ const ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?$/;
|
|
|
215
215
|
* @param {string} configDir an install root, e.g. `<project>/.doflow` or `~/.doflow`
|
|
216
216
|
* @returns {string}
|
|
217
217
|
*/
|
|
218
|
-
function runsDir(configDir
|
|
218
|
+
function runsDir(configDir ) {
|
|
219
219
|
return path.join(configDir, 'state', RUNS_DIRNAME);
|
|
220
220
|
}
|
|
221
221
|
|
|
@@ -234,8 +234,8 @@ function runsDir(configDir: string): string {
|
|
|
234
234
|
* subdirectory, so a project with state but no guidance tree falls through to the home install
|
|
235
235
|
* @returns {{dir: string, configDir: string, origin: string, searched: string[]}}
|
|
236
236
|
*/
|
|
237
|
-
function resolveRunsLocation({ start, global: forceGlobal = false, env = process.env, homeDir = os.homedir(), fsImpl = fs, containing = '' }
|
|
238
|
-
const searched
|
|
237
|
+
function resolveRunsLocation({ start, global: forceGlobal = false, env = process.env, homeDir = os.homedir(), fsImpl = fs, containing = '' } = {}) {
|
|
238
|
+
const searched = [];
|
|
239
239
|
const home = path.resolve(homeDir || '.');
|
|
240
240
|
if (forceGlobal) {
|
|
241
241
|
const configDir = path.join(home, '.doflow');
|
|
@@ -274,14 +274,14 @@ function resolveRunsLocation({ start, global: forceGlobal = false, env = process
|
|
|
274
274
|
* fields and never carries their values — a rejection report that echoed the value would leak
|
|
275
275
|
* exactly the content the rejection exists to keep out.
|
|
276
276
|
*/
|
|
277
|
-
function sanitizeRunEvent(input
|
|
277
|
+
function sanitizeRunEvent(input , { now = new Date() } = {}) {
|
|
278
278
|
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
279
279
|
return { record: null, dropped: [], reason: 'event must be an object' };
|
|
280
280
|
}
|
|
281
281
|
const byKey = new Map(RUN_FIELDS.map((field) => [field.key, field]));
|
|
282
282
|
const byWire = new Map(RUN_FIELDS.map((field) => [field.wire, field]));
|
|
283
|
-
const record
|
|
284
|
-
const dropped
|
|
283
|
+
const record = {};
|
|
284
|
+
const dropped = [];
|
|
285
285
|
|
|
286
286
|
const timestamp = typeof input.timestamp === 'string' && ISO_TIMESTAMP.test(input.timestamp)
|
|
287
287
|
? input.timestamp
|
|
@@ -317,9 +317,9 @@ function sanitizeRunEvent(input: any, { now = new Date() } = {}): any {
|
|
|
317
317
|
* @param {*} raw parsed JSON value
|
|
318
318
|
* @returns {Object|null} null when the line is not a usable record
|
|
319
319
|
*/
|
|
320
|
-
function normalizeRunRecord(raw
|
|
320
|
+
function normalizeRunRecord(raw ) {
|
|
321
321
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
322
|
-
const out
|
|
322
|
+
const out = { timestamp: typeof raw.timestamp === 'string' ? raw.timestamp : null };
|
|
323
323
|
for (const field of RUN_FIELDS) {
|
|
324
324
|
const value = raw[field.wire] !== undefined ? raw[field.wire] : raw[field.key];
|
|
325
325
|
if (value === undefined || value === null) continue;
|
|
@@ -337,9 +337,9 @@ function normalizeRunRecord(raw: any): any | null {
|
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
class RunLedger {
|
|
340
|
-
fsImpl
|
|
341
|
-
clock
|
|
342
|
-
dir
|
|
340
|
+
fsImpl ;
|
|
341
|
+
clock ;
|
|
342
|
+
dir ;
|
|
343
343
|
|
|
344
344
|
/**
|
|
345
345
|
* @param {Object} [options]
|
|
@@ -348,7 +348,7 @@ class RunLedger {
|
|
|
348
348
|
* @param {Object} [options.fsImpl=fs]
|
|
349
349
|
* @param {Function} [options.clock] returns the current Date; injection seam for tests
|
|
350
350
|
*/
|
|
351
|
-
constructor({ dir, configDir, fsImpl = fs, clock = () => new Date() }
|
|
351
|
+
constructor({ dir, configDir, fsImpl = fs, clock = () => new Date() } = {}) {
|
|
352
352
|
this.fsImpl = fsImpl;
|
|
353
353
|
this.clock = clock;
|
|
354
354
|
if (dir) this.dir = path.resolve(dir);
|
|
@@ -362,7 +362,7 @@ class RunLedger {
|
|
|
362
362
|
* @param {Object} event
|
|
363
363
|
* @returns {{written: boolean, file?: string, record?: Object, dropped: string[], reason?: string}}
|
|
364
364
|
*/
|
|
365
|
-
append(event
|
|
365
|
+
append(event ) {
|
|
366
366
|
const { record, dropped, reason } = sanitizeRunEvent(event, { now: this.clock() });
|
|
367
367
|
if (!record) return { written: false, dropped, reason };
|
|
368
368
|
try {
|
|
@@ -370,7 +370,7 @@ class RunLedger {
|
|
|
370
370
|
const file = path.join(this.dir, `${record.timestamp.slice(0, 10)}.jsonl`);
|
|
371
371
|
this.fsImpl.appendFileSync(file, `${JSON.stringify(record)}\n`, 'utf8');
|
|
372
372
|
return { written: true, file, record, dropped };
|
|
373
|
-
} catch (error
|
|
373
|
+
} catch (error ) {
|
|
374
374
|
return { written: false, record, dropped, reason: error.message };
|
|
375
375
|
}
|
|
376
376
|
}
|
|
@@ -383,8 +383,8 @@ class RunLedger {
|
|
|
383
383
|
* @param {number} [options.days] keep only partitions dated within the last N calendar days
|
|
384
384
|
* @returns {{dir: string, exists: boolean, files: string[], records: Object[], malformedLines: number, unreadableFiles: string[], days: number|null}}
|
|
385
385
|
*/
|
|
386
|
-
read({ days = null }
|
|
387
|
-
const result
|
|
386
|
+
read({ days = null } = {}) {
|
|
387
|
+
const result = {
|
|
388
388
|
dir: this.dir,
|
|
389
389
|
exists: false,
|
|
390
390
|
files: [],
|
|
@@ -394,7 +394,7 @@ class RunLedger {
|
|
|
394
394
|
days: days && days > 0 ? days : null,
|
|
395
395
|
};
|
|
396
396
|
|
|
397
|
-
let entries
|
|
397
|
+
let entries ;
|
|
398
398
|
try {
|
|
399
399
|
entries = this.fsImpl.readdirSync(this.dir);
|
|
400
400
|
} catch {
|
|
@@ -402,7 +402,7 @@ class RunLedger {
|
|
|
402
402
|
}
|
|
403
403
|
result.exists = true;
|
|
404
404
|
|
|
405
|
-
let names
|
|
405
|
+
let names = entries.filter((name) => LEDGER_FILE_RE.test(name)).sort();
|
|
406
406
|
if (result.days) {
|
|
407
407
|
// Calendar days, not "the last N files": a quiet week must not silently widen the window.
|
|
408
408
|
const cutoff = new Date(this.clock().getTime() - (result.days - 1) * 24 * 60 * 60 * 1000)
|
|
@@ -412,7 +412,7 @@ class RunLedger {
|
|
|
412
412
|
result.files = names;
|
|
413
413
|
|
|
414
414
|
for (const name of names) {
|
|
415
|
-
let text
|
|
415
|
+
let text ;
|
|
416
416
|
try {
|
|
417
417
|
text = this.fsImpl.readFileSync(path.join(this.dir, name), 'utf8');
|
|
418
418
|
} catch {
|
|
@@ -421,7 +421,7 @@ class RunLedger {
|
|
|
421
421
|
}
|
|
422
422
|
for (const line of text.split('\n')) {
|
|
423
423
|
if (line.trim() === '') continue;
|
|
424
|
-
let parsed
|
|
424
|
+
let parsed ;
|
|
425
425
|
try {
|
|
426
426
|
parsed = JSON.parse(line);
|
|
427
427
|
} catch {
|
|
@@ -436,7 +436,7 @@ class RunLedger {
|
|
|
436
436
|
}
|
|
437
437
|
|
|
438
438
|
// Undated records sort last rather than being dropped: they still count toward usage.
|
|
439
|
-
result.records.sort((a
|
|
439
|
+
result.records.sort((a , b ) => (a.at ?? Number.MAX_SAFE_INTEGER) - (b.at ?? Number.MAX_SAFE_INTEGER));
|
|
440
440
|
return result;
|
|
441
441
|
}
|
|
442
442
|
}
|
|
@@ -455,7 +455,7 @@ class RunLedger {
|
|
|
455
455
|
* @param {string} [options.projectRoot]
|
|
456
456
|
* @returns {number} process exit code
|
|
457
457
|
*/
|
|
458
|
-
function handleTraceCommand({ json = false, days = null, global: useGlobal = false, projectRoot }
|
|
458
|
+
function handleTraceCommand({ json = false, days = null, global: useGlobal = false, projectRoot } = {}) {
|
|
459
459
|
const location = resolveRunsLocation({ start: projectRoot, global: useGlobal });
|
|
460
460
|
const view = buildTrace(new RunLedger({ dir: location.dir }).read({ days }));
|
|
461
461
|
view.origin = location.origin;
|
|
@@ -503,7 +503,7 @@ function handleTraceCommand({ json = false, days = null, global: useGlobal = fal
|
|
|
503
503
|
* @param {Object} options
|
|
504
504
|
* @returns {number} process exit code
|
|
505
505
|
*/
|
|
506
|
-
function handleStatsCommand({ json = false, days = null, global: useGlobal = false, projectRoot }
|
|
506
|
+
function handleStatsCommand({ json = false, days = null, global: useGlobal = false, projectRoot } = {}) {
|
|
507
507
|
const location = resolveRunsLocation({ start: projectRoot, global: useGlobal });
|
|
508
508
|
const view = buildStats(new RunLedger({ dir: location.dir }).read({ days }));
|
|
509
509
|
view.origin = location.origin;
|
|
@@ -556,7 +556,7 @@ function handleStatsCommand({ json = false, days = null, global: useGlobal = fal
|
|
|
556
556
|
* @param {Object} [options.providerHealth] injected for tests; probed from `health.ts` otherwise
|
|
557
557
|
* @returns {number} process exit code — 1 when there is a finding to act on
|
|
558
558
|
*/
|
|
559
|
-
function handleDiscoverCommand({ json = false, days = null, global: useGlobal = false, projectRoot, providerHealth }
|
|
559
|
+
function handleDiscoverCommand({ json = false, days = null, global: useGlobal = false, projectRoot, providerHealth } = {}) {
|
|
560
560
|
const location = resolveRunsLocation({ start: projectRoot, global: useGlobal });
|
|
561
561
|
const read = new RunLedger({ dir: location.dir }).read({ days });
|
|
562
562
|
|
|
@@ -565,7 +565,7 @@ function handleDiscoverCommand({ json = false, days = null, global: useGlobal =
|
|
|
565
565
|
let health = providerHealth;
|
|
566
566
|
if (health === undefined) {
|
|
567
567
|
try {
|
|
568
|
-
health = require('../health.
|
|
568
|
+
health = require('../health.js').probeProviders({ ids: Object.values(PROVIDER) });
|
|
569
569
|
} catch {
|
|
570
570
|
health = null;
|
|
571
571
|
}
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
/** ISO timestamp to a compact local-independent `MM-DD HH:MM:SS`. Keeps columns narrow without
|
|
14
14
|
* inventing a timezone the record does not carry. */
|
|
15
|
-
function shortTime(timestamp
|
|
15
|
+
function shortTime(timestamp ) {
|
|
16
16
|
if (typeof timestamp !== 'string' || timestamp.length < 19) return '(undated)';
|
|
17
17
|
return `${timestamp.slice(5, 10)} ${timestamp.slice(11, 19)}`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function formatMs(value
|
|
20
|
+
function formatMs(value ) {
|
|
21
21
|
if (typeof value !== 'number') return '-';
|
|
22
22
|
if (value < 1000) return `${value}ms`;
|
|
23
23
|
return `${(value / 1000).toFixed(1)}s`;
|
|
@@ -33,7 +33,7 @@ function formatMs(value: any): string {
|
|
|
33
33
|
* @param {number} code
|
|
34
34
|
* @returns {number}
|
|
35
35
|
*/
|
|
36
|
-
function finish(code
|
|
36
|
+
function finish(code ) {
|
|
37
37
|
process.exitCode = code;
|
|
38
38
|
return code;
|
|
39
39
|
}
|
|
@@ -44,22 +44,22 @@ function finish(code: number): number {
|
|
|
44
44
|
* @param {Object} ledger
|
|
45
45
|
* @returns {string|null}
|
|
46
46
|
*/
|
|
47
|
-
function integrityNote(ledger
|
|
48
|
-
const parts
|
|
47
|
+
function integrityNote(ledger ) {
|
|
48
|
+
const parts = [];
|
|
49
49
|
if (ledger.malformedLines) parts.push(`${ledger.malformedLines} unreadable ledger line(s) skipped`);
|
|
50
50
|
if (ledger.unreadableFiles.length) parts.push(`${ledger.unreadableFiles.length} unreadable partition(s): ${ledger.unreadableFiles.join(', ')}`);
|
|
51
51
|
return parts.length ? `Note: ${parts.join('; ')}.` : null;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/** One place that turns a resolved location into the sentence an empty ledger deserves. */
|
|
55
|
-
function printEmptyLedgerNotice(ledger
|
|
55
|
+
function printEmptyLedgerNotice(ledger , reason ) {
|
|
56
56
|
console.log(` ${reason}`);
|
|
57
57
|
console.log(` Location: ${ledger.dir}`);
|
|
58
58
|
console.log(' An empty ledger is a normal state for a fresh install. It is not evidence that');
|
|
59
59
|
console.log(' nothing ran, and no conclusion about usage can be drawn from it.');
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
const DISCOVER_MARK
|
|
62
|
+
const DISCOVER_MARK = Object.freeze({
|
|
63
63
|
FINDING: '! FINDING',
|
|
64
64
|
CLEAR: '✓ CLEAR',
|
|
65
65
|
NOT_DETERMINED: '? UNKNOWN',
|