@khoavu882/doflow 2.0.0-beta.3 → 2.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/doflow.js +2 -2
- package/core/.antigravity-plugin/plugin.json +0 -1
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
- package/core/harnesses/claude/hooks/session-start.sh +5 -3
- package/core/harnesses/claude/hooks/stop-check.sh +5 -3
- package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
- package/core/harnesses/codex/hooks/session-start.sh +5 -3
- package/core/harnesses/codex/hooks/stop-check.sh +6 -5
- package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
- package/core/harnesses/gemini/hooks/session-start.sh +5 -3
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
- package/core/harnesses/kiro/hooks/session-start.sh +11 -5
- package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
- package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
- package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
- package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
- package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
- package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
- package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
- package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
- package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
- package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
- package/core/registry/assets.json +19 -0
- package/core/registry/contracts.json +2 -2
- package/core/registry/harnesses.json +196 -10
- package/core/registry/lifecycle.json +2626 -7
- package/core/registry/storage.json +110 -83
- package/core/registry/verification.json +1 -1
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
- package/core/shared/scripts/doflow/bin/doflow-run +10 -6
- package/core/shared/skills/do-design/SKILL.md +6 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
- package/core/shared/skills/do-execute-plan/references/scaffold.md +2 -2
- package/core/shared/skills/do-implement/SKILL.md +3 -2
- package/core/shared/skills/do-plan/SKILL.md +7 -8
- package/package.json +4 -2
- package/src/adapters/antigravity/{index.ts → index.js} +141 -105
- package/src/adapters/claude/{index.ts → index.js} +63 -49
- package/src/adapters/claude/{mcp.ts → mcp.js} +16 -16
- package/src/adapters/codex/{agents.ts → agents.js} +24 -24
- package/src/adapters/codex/{config.ts → config.js} +49 -49
- package/src/adapters/codex/{hooks.ts → hooks.js} +62 -27
- package/src/adapters/codex/{index.ts → index.js} +148 -105
- package/src/adapters/codex/{mcp.ts → mcp.js} +38 -38
- package/src/adapters/codex/{native-projection.ts → native-projection.js} +5 -5
- package/src/adapters/copilot/{index.ts → index.js} +42 -42
- package/src/adapters/{copy-tree.ts → copy-tree.js} +74 -74
- package/src/adapters/gemini/{hooks.ts → hooks.js} +78 -37
- package/src/adapters/gemini/{index.ts → index.js} +96 -57
- package/src/adapters/{hook-commands.ts → hook-commands.js} +8 -8
- package/src/adapters/{index.ts → index.js} +13 -13
- package/src/adapters/kiro/{index.ts → index.js} +49 -35
- package/src/adapters/{mcp-entries.ts → mcp-entries.js} +55 -55
- package/src/adapters/opencode/{index.ts → index.js} +52 -52
- package/src/adapters/pi/hooks.js +119 -0
- package/src/adapters/pi/{index.ts → index.js} +64 -39
- package/src/adapters/pi/{json-members.ts → json-members.js} +23 -23
- package/src/adapters/pi/{mcp.ts → mcp.js} +70 -70
- package/src/adapters/{resolve-source.ts → resolve-source.js} +1 -1
- package/src/cli/commands/{install.ts → install.js} +25 -25
- package/src/cli/commands/{list-backups.ts → list-backups.js} +4 -4
- package/src/cli/commands/{reconcile.ts → reconcile.js} +19 -19
- package/src/cli/commands/{remove.ts → remove.js} +10 -10
- package/src/cli/commands/{rollback.ts → rollback.js} +17 -17
- package/src/cli/commands/{self-update.ts → self-update.js} +3 -3
- package/src/cli/commands/{status.ts → status.js} +24 -24
- package/src/cli/commands/{tools.ts → tools.js} +16 -16
- package/src/cli/commands/{update.ts → update.js} +20 -20
- package/src/cli/{index.ts → index.js} +43 -31
- package/src/cli/{runtime-commands.ts → runtime-commands.js} +97 -58
- package/src/cli/{shared.ts → shared.js} +40 -40
- package/src/helper/{bash-helper.ts → bash-helper.js} +7 -7
- package/src/helper/{git.ts → git.js} +2 -2
- package/src/helper/{guardrails.ts → guardrails.js} +10 -10
- package/src/helper/{harness-paths.ts → harness-paths.js} +24 -24
- package/src/helper/{marker-merge.ts → marker-merge.js} +4 -4
- package/src/helper/{prompt.ts → prompt.js} +5 -5
- package/src/helper/{repo-root.ts → repo-root.js} +2 -2
- package/src/helper/{settings-merge.ts → settings-merge.js} +27 -27
- package/src/helper/{settings-scope.ts → settings-scope.js} +3 -3
- package/src/helper/{toml.ts → toml.js} +48 -48
- package/src/install/{backup-set.ts → backup-set.js} +16 -16
- package/src/install/{backup.ts → backup.js} +65 -65
- package/src/install/{context.ts → context.js} +15 -15
- package/src/install/{manifest.ts → manifest.js} +27 -27
- package/src/install/{mcp.ts → mcp.js} +30 -30
- package/src/install/{paths.ts → paths.js} +15 -15
- package/src/install/{targets.ts → targets.js} +10 -10
- package/src/install/{tool-lifecycle.ts → tool-lifecycle.js} +20 -20
- package/src/lifecycle/{bash-availability.ts → bash-availability.js} +1 -1
- package/src/lifecycle/coverage.js +57 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/{index.ts → index.js} +160 -146
- package/src/lifecycle/{mcp-index.ts → mcp-index.js} +1 -1
- package/src/lifecycle/{policies.ts → policies.js} +11 -11
- package/src/lifecycle/{view.ts → view.js} +28 -28
- package/src/registry/{index.ts → index.js} +195 -66
- package/src/release/build-package.js +91 -0
- package/src/release/changelog-section.js +70 -0
- package/src/release/{sync-plugin-versions.ts → sync-plugin-versions.js} +4 -4
- package/src/runtime/advice/{cli.ts → cli.js} +31 -31
- package/src/runtime/advice/{client.ts → client.js} +44 -44
- package/src/runtime/advice/{config.ts → config.js} +16 -16
- package/src/runtime/advice/{doctor.ts → doctor.js} +32 -32
- package/src/runtime/advice/{endpoint-guard.ts → endpoint-guard.js} +20 -20
- package/src/runtime/advice/{kinds.ts → kinds.js} +16 -16
- package/src/runtime/advice/providers/{ollama-systemone.ts → ollama-systemone.js} +21 -21
- package/src/runtime/advice/{record.ts → record.js} +19 -19
- package/src/runtime/advice/{registry.ts → registry.js} +16 -16
- package/src/runtime/advice/{strength.ts → strength.js} +7 -7
- package/src/runtime/{brief-tools.ts → brief-tools.js} +50 -50
- package/src/runtime/{c4-project.ts → c4-project.js} +27 -27
- package/src/runtime/candidate.js +505 -0
- package/src/runtime/{capability-router.ts → capability-router.js} +39 -39
- package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
- package/src/runtime/{claims.ts → claims.js} +44 -35
- package/src/runtime/{cli-result.ts → cli-result.js} +15 -8
- package/src/runtime/{cli.ts → cli.js} +59 -292
- package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
- package/src/runtime/context/budget.js +203 -0
- package/src/runtime/context/compose.js +1367 -0
- package/src/runtime/context/delivery-store.js +102 -0
- package/src/runtime/context/mandatory.js +594 -0
- package/src/runtime/context/pointer.js +142 -0
- package/src/runtime/context-pack.js +1277 -0
- package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
- package/src/runtime/evidence-fields.js +39 -0
- package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +32 -25
- package/src/runtime/failure/{capture.ts → capture.js} +29 -29
- package/src/runtime/failure/{classifier.ts → classifier.js} +4 -4
- package/src/runtime/failure/{cli.ts → cli.js} +30 -30
- package/src/runtime/failure/{home.ts → home.js} +6 -6
- package/src/runtime/failure/{store.ts → store.js} +32 -32
- package/src/runtime/{feature-resolve.ts → feature-resolve.js} +6 -6
- package/src/runtime/{freshness.ts → freshness.js} +59 -22
- package/src/runtime/{health.ts → health.js} +72 -72
- package/src/runtime/{history-compactor.ts → history-compactor.js} +25 -25
- package/src/runtime/{implementation-gate.ts → implementation-gate.js} +30 -30
- package/src/runtime/inventory/{coverage.ts → coverage.js} +10 -10
- package/src/runtime/inventory/{identity.ts → identity.js} +2 -2
- package/src/runtime/inventory/{index.ts → index.js} +36 -36
- package/src/runtime/inventory/{precedence.ts → precedence.js} +2 -2
- package/src/runtime/inventory/{read-scopes.ts → read-scopes.js} +9 -9
- package/src/runtime/inventory/{siblings.ts → siblings.js} +15 -15
- package/src/runtime/knowledge/{chunker.ts → chunker.js} +12 -12
- package/src/runtime/knowledge/{hybrid.ts → hybrid.js} +17 -17
- package/src/runtime/knowledge/{index-store.ts → index-store.js} +23 -23
- package/src/runtime/knowledge/{retrieval.ts → retrieval.js} +27 -27
- package/src/runtime/{leak-scan.ts → leak-scan.js} +12 -12
- package/src/runtime/lifecycle/{cli.ts → cli.js} +32 -32
- package/src/runtime/lifecycle/{event-store.ts → event-store.js} +48 -48
- package/src/runtime/lifecycle/{fold.ts → fold.js} +41 -41
- package/src/runtime/lifecycle/{followup.ts → followup.js} +54 -54
- package/src/runtime/lifecycle/{goal.ts → goal.js} +50 -50
- package/src/runtime/lifecycle/{intent-writer.ts → intent-writer.js} +5 -5
- package/src/runtime/lifecycle/{overview.ts → overview.js} +47 -47
- package/src/runtime/lifecycle/{release.ts → release.js} +29 -29
- package/src/runtime/lifecycle/{report-store.ts → report-store.js} +25 -25
- package/src/runtime/lifecycle/{retention.ts → retention.js} +37 -37
- package/src/runtime/lifecycle/{root.ts → root.js} +9 -9
- package/src/runtime/lifecycle/{status.ts → status.js} +64 -64
- package/src/runtime/lifecycle/{store-upkeep.ts → store-upkeep.js} +46 -46
- package/src/runtime/{locator-resolve.ts → locator-resolve.js} +7 -7
- package/src/runtime/{mask.ts → mask.js} +22 -22
- package/src/runtime/{mcp-consultations.ts → mcp-consultations.js} +54 -54
- package/src/runtime/{mcp-reach.ts → mcp-reach.js} +24 -24
- package/src/runtime/{model-router.ts → model-router.js} +20 -20
- package/src/runtime/{outcome.ts → outcome.js} +27 -27
- package/src/runtime/{reach.ts → reach.js} +21 -21
- package/src/runtime/{readiness-record.ts → readiness-record.js} +12 -12
- package/src/runtime/{readiness.ts → readiness.js} +113 -43
- package/src/runtime/{recovery.ts → recovery.js} +15 -15
- package/src/runtime/{research-request.ts → research-request.js} +34 -34
- package/src/runtime/{retrieval-plan.ts → retrieval-plan.js} +453 -104
- package/src/runtime/retrieval-record.js +223 -0
- package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +22 -19
- package/src/runtime/scaffold/{fingerprint.ts → fingerprint.js} +3 -3
- package/src/runtime/scaffold/{generate.ts → generate.js} +109 -109
- package/src/runtime/scaffold/{languages.ts → languages.js} +34 -34
- package/src/runtime/source-version.js +289 -0
- package/src/runtime/storage/{cli.ts → cli.js} +9 -9
- package/src/runtime/storage/{engine.ts → engine.js} +37 -37
- package/src/runtime/storage/{registry.ts → registry.js} +19 -19
- package/src/runtime/storage/{roots.ts → roots.js} +8 -8
- package/src/runtime/storage/{rules.ts → rules.js} +26 -26
- package/src/runtime/storage/{upkeep.ts → upkeep.js} +35 -35
- package/src/runtime/storage/{work-status.ts → work-status.js} +39 -39
- package/src/runtime/{task-attempts.ts → task-attempts.js} +62 -62
- package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
- package/src/runtime/{task-scope.ts → task-scope.js} +40 -15
- package/src/runtime/{task-state.ts → task-state.js} +86 -21
- package/src/runtime/trace/{indicators.ts → indicators.js} +22 -22
- package/src/runtime/trace/{ledger.ts → ledger.js} +42 -42
- package/src/runtime/trace/{render.ts → render.js} +7 -7
- package/src/runtime/trace/{views.ts → views.js} +40 -40
- package/src/runtime/verification/{contract-runner.ts → contract-runner.js} +23 -23
- package/src/runtime/verification/{engine.ts → engine.js} +143 -143
- package/src/runtime/verification/{registry.ts → registry.js} +6 -6
- package/src/runtime/verification/{scope-bound.ts → scope-bound.js} +50 -50
- package/src/runtime/verification/{test-selection.ts → test-selection.js} +28 -28
- package/src/runtime/{workflow-engine.ts → workflow-engine.js} +50 -50
- package/src/runtime/{workflow-orchestrator.ts → workflow-orchestrator.js} +91 -91
- package/src/runtime/{worktree.ts → worktree.js} +31 -31
- package/src/state/{index.ts → index.js} +26 -26
- package/src/state/{lockfile.ts → lockfile.js} +20 -20
- package/src/state/{run-lock.ts → run-lock.js} +44 -44
- package/core/.antigravity-plugin/hooks.json +0 -39
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
- package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
- package/core/harnesses/shared/hooks/hooks.json +0 -47
- package/src/runtime/context-pack.ts +0 -492
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
const fs = require('node:fs');
|
|
18
18
|
const os = require('node:os');
|
|
19
19
|
const path = require('node:path');
|
|
20
|
-
const { REPO_ROOT } = require('../../helper/repo-root.
|
|
21
|
-
const { normaliseMessage, maskLine } = require('../mask.
|
|
22
|
-
const { isProgrammingError, errorKind } = require('./classifier.
|
|
23
|
-
const { failureHome, captureIsOff, eventsPath } = require('./home.
|
|
20
|
+
const { REPO_ROOT } = require('../../helper/repo-root.js');
|
|
21
|
+
const { normaliseMessage, maskLine } = require('../mask.js');
|
|
22
|
+
const { isProgrammingError, errorKind } = require('./classifier.js');
|
|
23
|
+
const { failureHome, captureIsOff, eventsPath } = require('./home.js');
|
|
24
24
|
|
|
25
25
|
const MAX_LINE_BYTES = 2048;
|
|
26
26
|
const MAX_RAW_MESSAGE = 4096;
|
|
@@ -33,24 +33,24 @@ const COMMAND_NAME = /^[a-z][a-z0-9-]{0,39}$/;
|
|
|
33
33
|
/** Errors already recorded in this process, so one error caught at two points counts once. */
|
|
34
34
|
const recorded = new WeakSet();
|
|
35
35
|
|
|
36
|
-
let cachedVersion
|
|
36
|
+
let cachedVersion = null;
|
|
37
37
|
/** The package version; the projected runtime ships no package.json, so a tolerant read. */
|
|
38
|
-
function packageVersion()
|
|
38
|
+
function packageVersion() {
|
|
39
39
|
if (cachedVersion === null) {
|
|
40
40
|
try { cachedVersion = String(require('../../../package.json').version); } catch { cachedVersion = 'unknown'; }
|
|
41
41
|
}
|
|
42
42
|
return cachedVersion;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function harnessName(env
|
|
45
|
+
function harnessName(env ) {
|
|
46
46
|
const raw = env.DOFLOW_AGENT;
|
|
47
47
|
return typeof raw === 'string' && /^[A-Za-z0-9._-]{1,40}$/.test(raw) ? raw : 'none';
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/** The absolute, non-root homes to compare a working directory with: HOME as given, its real path, and `os.homedir()`. */
|
|
51
|
-
function homeCandidates(env
|
|
52
|
-
const out
|
|
53
|
-
const add = (value
|
|
51
|
+
function homeCandidates(env ) {
|
|
52
|
+
const out = [];
|
|
53
|
+
const add = (value ) => {
|
|
54
54
|
if (typeof value !== 'string' || !path.isAbsolute(value)) return;
|
|
55
55
|
const home = value.replace(/\/+$/, '');
|
|
56
56
|
if (home.length > 1 && !out.includes(home)) out.push(home);
|
|
@@ -67,8 +67,8 @@ function homeCandidates(env: any): string[] {
|
|
|
67
67
|
* directory under no known home, when `HOME` itself is not usable, is stored as its folder name only,
|
|
68
68
|
* so an absolute path with a user name in it never reaches the file.
|
|
69
69
|
*/
|
|
70
|
-
function projectName(env
|
|
71
|
-
let cwd
|
|
70
|
+
function projectName(env ) {
|
|
71
|
+
let cwd ;
|
|
72
72
|
try { cwd = process.cwd(); } catch { return ''; }
|
|
73
73
|
const home = homeCandidates(env).find((h) => cwd === h || cwd.startsWith(`${h}/`));
|
|
74
74
|
if (home) return maskLine(`~${cwd.slice(home.length)}`, { home: null }).text.slice(0, 200);
|
|
@@ -77,7 +77,7 @@ function projectName(env: any): string {
|
|
|
77
77
|
return maskLine(cwd, { home: env.HOME }).text.slice(0, 200);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function commandName(command
|
|
80
|
+
function commandName(command ) {
|
|
81
81
|
return typeof command === 'string' && COMMAND_NAME.test(command) ? command : 'unknown';
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -88,8 +88,8 @@ function commandName(command: any): string {
|
|
|
88
88
|
* @param {*} error
|
|
89
89
|
* @returns {string|null}
|
|
90
90
|
*/
|
|
91
|
-
function doflowFrame(error
|
|
92
|
-
let stack
|
|
91
|
+
function doflowFrame(error ) {
|
|
92
|
+
let stack ;
|
|
93
93
|
try { stack = typeof error.stack === 'string' ? error.stack : ''; } catch { return null; }
|
|
94
94
|
const root = REPO_ROOT + path.sep;
|
|
95
95
|
for (const line of stack.split('\n')) {
|
|
@@ -97,15 +97,15 @@ function doflowFrame(error: any): string | null {
|
|
|
97
97
|
if (!match) continue;
|
|
98
98
|
const file = match[2].replace(/^file:\/\//, '');
|
|
99
99
|
if (!file.startsWith(root) || file.includes(`${path.sep}node_modules${path.sep}`)) continue;
|
|
100
|
-
const fn = (match[1] || '').replace(/^new /, '').split('.').pop()
|
|
100
|
+
const fn = (match[1] || '').replace(/^new /, '').split('.').pop() .replace(/\s.*$/, '') || '<anonymous>';
|
|
101
101
|
return `${path.relative(REPO_ROOT, file).split(path.sep).join('/')}:${fn}`;
|
|
102
102
|
}
|
|
103
103
|
return null;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/** One line at or under MAX_LINE_BYTES including the newline: shorten `message`, then `project`. */
|
|
107
|
-
function buildLine(record
|
|
108
|
-
const bytes = (r
|
|
107
|
+
function buildLine(record ) {
|
|
108
|
+
const bytes = (r ) => Buffer.byteLength(JSON.stringify(r), 'utf8') + 1;
|
|
109
109
|
const out = { ...record };
|
|
110
110
|
for (const field of ['message', 'project']) {
|
|
111
111
|
while (bytes(out) > MAX_LINE_BYTES && out[field].length > 0) {
|
|
@@ -115,23 +115,23 @@ function buildLine(record: any): string | null {
|
|
|
115
115
|
return bytes(out) > MAX_LINE_BYTES ? null : `${JSON.stringify(out)}\n`;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
const stamp = (date
|
|
118
|
+
const stamp = (date ) => date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
|
119
119
|
|
|
120
120
|
/** Rotated files with their size and modification time, oldest last-modified first (names alone tie inside one second). */
|
|
121
|
-
function rotatedFiles(home
|
|
122
|
-
return fs.readdirSync(home).filter((name
|
|
121
|
+
function rotatedFiles(home ) {
|
|
122
|
+
return fs.readdirSync(home).filter((name ) => ROTATED.test(name)).map((name ) => {
|
|
123
123
|
let size = 0;
|
|
124
124
|
let mtime = 0;
|
|
125
125
|
try { const st = fs.statSync(path.join(home, name)); size = st.size; mtime = st.mtimeMs; } catch { /* gone */ }
|
|
126
126
|
return { name, size, mtime };
|
|
127
|
-
}).sort((a
|
|
127
|
+
}).sort((a , b ) => a.mtime - b.mtime || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
131
|
* Removes rotated files beyond the newest four full ones, and partial ones (see rotateIfDue) once
|
|
132
132
|
* they are an hour old. A partial file never counts toward the four, so it cannot push a real one out.
|
|
133
133
|
*/
|
|
134
|
-
function prune(home
|
|
134
|
+
function prune(home , now = Date.now()) {
|
|
135
135
|
const files = rotatedFiles(home);
|
|
136
136
|
const full = files.filter((f) => f.size >= ROTATE_AT_BYTES);
|
|
137
137
|
const doomed = [
|
|
@@ -157,7 +157,7 @@ function prune(home: string, now = Date.now()): void {
|
|
|
157
157
|
* @param {string} home the failure home
|
|
158
158
|
* @returns {boolean} whether this call rotated a full file
|
|
159
159
|
*/
|
|
160
|
-
function rotateIfDue(home
|
|
160
|
+
function rotateIfDue(home ) {
|
|
161
161
|
const live = eventsPath(home);
|
|
162
162
|
try {
|
|
163
163
|
if (fs.statSync(live).size < ROTATE_AT_BYTES) return false;
|
|
@@ -188,9 +188,9 @@ function rotateIfDue(home: string): boolean {
|
|
|
188
188
|
* descriptor is checked before the write, so a file swapped in after any earlier check is still safe.
|
|
189
189
|
* @returns {boolean} whether the line was written
|
|
190
190
|
*/
|
|
191
|
-
function appendRegular(file
|
|
191
|
+
function appendRegular(file , line ) {
|
|
192
192
|
const c = fs.constants;
|
|
193
|
-
let fd
|
|
193
|
+
let fd ;
|
|
194
194
|
try {
|
|
195
195
|
fd = fs.openSync(file, c.O_WRONLY | c.O_APPEND | c.O_CREAT | (c.O_NOFOLLOW || 0) | (c.O_NONBLOCK || 0), 0o600);
|
|
196
196
|
if (!fs.fstatSync(fd).isFile()) return false;
|
|
@@ -208,7 +208,7 @@ function appendRegular(file: string, line: string): boolean {
|
|
|
208
208
|
* @param {Object} [env] defaults to process.env
|
|
209
209
|
* @returns {boolean} whether a line was written
|
|
210
210
|
*/
|
|
211
|
-
function captureFailure(fields
|
|
211
|
+
function captureFailure(fields , env = process.env) {
|
|
212
212
|
try {
|
|
213
213
|
const home = failureHome(env);
|
|
214
214
|
if (!home || captureIsOff(home, env)) return false;
|
|
@@ -235,7 +235,7 @@ function captureFailure(fields: any, env = process.env): boolean {
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
/** The error's message cut to 4096 characters before any masking, so no input makes the rules do unbounded work. */
|
|
238
|
-
function rawMessage(error
|
|
238
|
+
function rawMessage(error ) {
|
|
239
239
|
try {
|
|
240
240
|
const message = error.message;
|
|
241
241
|
return typeof message === 'string' ? message.slice(0, MAX_RAW_MESSAGE) : '';
|
|
@@ -251,7 +251,7 @@ function rawMessage(error: any): string {
|
|
|
251
251
|
* @param {Object} [env]
|
|
252
252
|
* @returns {boolean} whether a line was written
|
|
253
253
|
*/
|
|
254
|
-
function captureError(error
|
|
254
|
+
function captureError(error , context = {}, env = process.env) {
|
|
255
255
|
try {
|
|
256
256
|
if (!isProgrammingError(error) || recorded.has(error)) return false;
|
|
257
257
|
recorded.add(error);
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
const CLASS_NAMES = new Set(['TypeError', 'RangeError', 'ReferenceError']);
|
|
15
15
|
|
|
16
16
|
/** Reads one property without ever throwing: a throwing getter reads as undefined. */
|
|
17
|
-
function read(error
|
|
17
|
+
function read(error , key ) {
|
|
18
18
|
try { return error[key]; } catch { return undefined; }
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/** `error instanceof Class`, never throwing. */
|
|
22
|
-
function isA(error
|
|
22
|
+
function isA(error , Class ) {
|
|
23
23
|
try { return error instanceof Class; } catch { return false; }
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -29,7 +29,7 @@ function isA(error: any, Class: any): boolean {
|
|
|
29
29
|
* @param {*} error
|
|
30
30
|
* @returns {boolean}
|
|
31
31
|
*/
|
|
32
|
-
function isProgrammingError(error
|
|
32
|
+
function isProgrammingError(error ) {
|
|
33
33
|
try {
|
|
34
34
|
if (error === null || typeof error !== 'object') return false;
|
|
35
35
|
const code = read(error, 'code');
|
|
@@ -53,7 +53,7 @@ function isProgrammingError(error: any): boolean {
|
|
|
53
53
|
* @param {*} error
|
|
54
54
|
* @returns {string}
|
|
55
55
|
*/
|
|
56
|
-
function errorKind(error
|
|
56
|
+
function errorKind(error ) {
|
|
57
57
|
for (const Class of [TypeError, RangeError, ReferenceError]) {
|
|
58
58
|
if (isA(error, Class)) return Class.name;
|
|
59
59
|
}
|
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
const fs = require('node:fs');
|
|
15
|
-
const { finishRuntime, usageError } = require('../cli-result.
|
|
16
|
-
const { maskLine, printSafe } = require('../mask.
|
|
17
|
-
const { failureHome, captureSwitch, sentinelPath } = require('./home.
|
|
18
|
-
const { rotateIfDue } = require('./capture.
|
|
19
|
-
const store = require('./store.
|
|
20
|
-
const { projectRoot } = require('../lifecycle/root.
|
|
21
|
-
const { appendEvents, readFold, randomChars, StoreUnsafeError, StoreLockedError } = require('../lifecycle/event-store.
|
|
22
|
-
const { prepareStore } = require('../lifecycle/store-upkeep.
|
|
15
|
+
const { finishRuntime, usageError } = require('../cli-result.js');
|
|
16
|
+
const { maskLine, printSafe } = require('../mask.js');
|
|
17
|
+
const { failureHome, captureSwitch, sentinelPath } = require('./home.js');
|
|
18
|
+
const { rotateIfDue } = require('./capture.js');
|
|
19
|
+
const store = require('./store.js');
|
|
20
|
+
const { projectRoot } = require('../lifecycle/root.js');
|
|
21
|
+
const { appendEvents, readFold, randomChars, StoreUnsafeError, StoreLockedError } = require('../lifecycle/event-store.js');
|
|
22
|
+
const { prepareStore } = require('../lifecycle/store-upkeep.js');
|
|
23
23
|
|
|
24
24
|
const ACTIONS = ['list', 'settle', 'capture'];
|
|
25
25
|
const STATEMENT_MAX = 280;
|
|
@@ -29,10 +29,10 @@ const CONTROL = /[\u0000-\u001F\u007F-\u009F\u2028\u2029\u202A-\u202E\u2066-\u20
|
|
|
29
29
|
/** A caller mistake (exit 2). */
|
|
30
30
|
class FailureUsageError extends Error {}
|
|
31
31
|
|
|
32
|
-
const refusal = (action
|
|
32
|
+
const refusal = (action , finding , message ) => ({ ok: false, action, finding, message });
|
|
33
33
|
|
|
34
34
|
/** A one-line reason, masked like any other free text, or null when none was given. */
|
|
35
|
-
function cleanReason(raw
|
|
35
|
+
function cleanReason(raw ) {
|
|
36
36
|
if (raw === undefined) return null;
|
|
37
37
|
const rawLength = String(raw).length;
|
|
38
38
|
if (rawLength > STATEMENT_MAX * 4) throw new FailureUsageError(`--reason is ${rawLength} characters; the limit is ${STATEMENT_MAX}`);
|
|
@@ -44,19 +44,19 @@ function cleanReason(raw: any): string | null {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/** `DoFlow <command> <kind>: <message>`, built only from stored fields, masked and cut to 280 characters. */
|
|
47
|
-
function importedStatement(entry
|
|
47
|
+
function importedStatement(entry ) {
|
|
48
48
|
const raw = `DoFlow ${entry.command} ${entry.kind}${entry.message ? `: ${entry.message}` : ''}`;
|
|
49
49
|
return maskLine(raw.replace(CONTROL, ' ')).text.replace(/\s+/g, ' ').trim().slice(0, STATEMENT_MAX);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// ── list ───────────────────────────────────────────────────────────────────────────────────────
|
|
53
53
|
|
|
54
|
-
function listFailures({ all = false, env = process.env } = {})
|
|
54
|
+
function listFailures({ all = false, env = process.env } = {}) {
|
|
55
55
|
const home = failureHome(env);
|
|
56
56
|
if (home) rotateIfDue(home);
|
|
57
57
|
const loaded = store.loadEntries({ env });
|
|
58
|
-
const shown = loaded.entries.filter((entry
|
|
59
|
-
const result
|
|
58
|
+
const shown = loaded.entries.filter((entry ) => all || store.SHOWN_BY_DEFAULT.has(entry.status)).map(store.listedEntry);
|
|
59
|
+
const result = { ok: true, action: 'list', capture: loaded.capture };
|
|
60
60
|
if (loaded.home === null) result.home = null;
|
|
61
61
|
result.entries = shown;
|
|
62
62
|
result.counts = loaded.counts;
|
|
@@ -70,7 +70,7 @@ function listFailures({ all = false, env = process.env } = {}): any {
|
|
|
70
70
|
|
|
71
71
|
// ── settle ─────────────────────────────────────────────────────────────────────────────────────
|
|
72
72
|
|
|
73
|
-
function settleFailure({ fp, as, reason, cwd, env = process.env, now = new Date() }
|
|
73
|
+
function settleFailure({ fp, as, reason, cwd, env = process.env, now = new Date() } ) {
|
|
74
74
|
if (typeof fp !== 'string' || !FP.test(fp)) throw new FailureUsageError('--fp is required: the 16 hex characters of an entry from `failure --action list`');
|
|
75
75
|
if (!store.SETTLE_AS.includes(as)) throw new FailureUsageError(`--as must be one of ${store.SETTLE_AS.join(', ')}${as ? ` (got '${as}')` : ''}`);
|
|
76
76
|
const cleaned = cleanReason(reason);
|
|
@@ -88,7 +88,7 @@ function settleFailure({ fp, as, reason, cwd, env = process.env, now = new Date(
|
|
|
88
88
|
for (const text of prepared.lines) console.error(printSafe(`doflow failure: ${text}`));
|
|
89
89
|
if (!prepared.ok) return refusal('settle', prepared.finding, prepared.message);
|
|
90
90
|
}
|
|
91
|
-
const entry = store.loadEntries({ env }).entries.find((e
|
|
91
|
+
const entry = store.loadEntries({ env }).entries.find((e ) => e.fp === fp);
|
|
92
92
|
if (!entry) return refusal('settle', 'unknown-fp', `${fp} is not a failure entry on this machine; list them with doflow-run failure --action list. Nothing was written.`);
|
|
93
93
|
|
|
94
94
|
if (as !== 'imported') {
|
|
@@ -98,10 +98,10 @@ function settleFailure({ fp, as, reason, cwd, env = process.env, now = new Date(
|
|
|
98
98
|
// Deduplicated on the project's own follow-ups, whatever the settlements say: a follow-up that
|
|
99
99
|
// already comes from this fingerprint is never created twice, even after a noise or fixed settlement.
|
|
100
100
|
const followups = readFold(root, { now }).followups;
|
|
101
|
-
const existing = followups.find((f
|
|
101
|
+
const existing = followups.find((f ) => f.source && f.source.kind === 'failure' && f.source.ref === fp);
|
|
102
102
|
if (existing) return refusal('settle', 'already-imported', `${fp} was already imported as ${existing.id}. Nothing was written.`);
|
|
103
|
-
const taken = new Set(followups.map((f
|
|
104
|
-
let id
|
|
103
|
+
const taken = new Set(followups.map((f ) => f.id));
|
|
104
|
+
let id ;
|
|
105
105
|
do { id = `FU-${randomChars(6)}`; } while (taken.has(id));
|
|
106
106
|
const statement = importedStatement(entry);
|
|
107
107
|
const source = { kind: 'failure', ref: fp };
|
|
@@ -113,13 +113,13 @@ function settleFailure({ fp, as, reason, cwd, env = process.env, now = new Date(
|
|
|
113
113
|
return {
|
|
114
114
|
ok: true, action: 'settle', fp, as,
|
|
115
115
|
followup: { id, statement, source, state: 'open' },
|
|
116
|
-
events: out.written.map((w
|
|
116
|
+
events: out.written.map((w ) => w.file),
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
// ── capture ────────────────────────────────────────────────────────────────────────────────────
|
|
121
121
|
|
|
122
|
-
function captureState({ set, env = process.env, now = new Date() }
|
|
122
|
+
function captureState({ set, env = process.env, now = new Date() } = {}) {
|
|
123
123
|
if (set !== undefined && set !== 'on' && set !== 'off') throw new FailureUsageError(`--set must be on or off (got '${set}')`);
|
|
124
124
|
const home = failureHome(env);
|
|
125
125
|
if (home !== null && set === 'off') {
|
|
@@ -130,21 +130,21 @@ function captureState({ set, env = process.env, now = new Date() }: any = {}): a
|
|
|
130
130
|
}
|
|
131
131
|
const state = captureSwitch(home, env);
|
|
132
132
|
// With no failure home nothing is captured, whatever the switch says.
|
|
133
|
-
const result
|
|
133
|
+
const result = { ok: true, action: 'capture', effective: home === null ? 'off' : state.effective, sentinel: state.sentinel, env: state.env };
|
|
134
134
|
if (home === null) result.home = null;
|
|
135
135
|
return result;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
// ── verb ───────────────────────────────────────────────────────────────────────────────────────
|
|
139
139
|
|
|
140
|
-
function lines(result
|
|
140
|
+
function lines(result ) {
|
|
141
141
|
if (result.home === null) return ['home: none (capture skipped)', 'capture: off'];
|
|
142
142
|
if (result.action === 'list') {
|
|
143
143
|
const out = [`capture ${result.capture}: ${result.entries.length} entr${result.entries.length === 1 ? 'y' : 'ies'} listed (new ${result.counts.new}, regressed ${result.counts.regressed}, noise ${result.counts.noise}, fixed ${result.counts.fixed}, imported ${result.counts.imported})`];
|
|
144
144
|
for (const e of result.entries) out.push(`${e.fp} ${e.status} x${e.count} ${e.command} ${e.kind}${e.message ? `: ${e.message}` : ''} (last seen ${e.lastSeen}, ${e.lastVersion})`);
|
|
145
145
|
if (result.unreadable.length) out.push(`unreadable failure files (not read): ${result.unreadable.join(', ')}`);
|
|
146
146
|
if (result.skippedLines) out.push(`${result.skippedLines} unreadable line${result.skippedLines === 1 ? '' : 's'} skipped`);
|
|
147
|
-
return [...out, ...result.next.map((n
|
|
147
|
+
return [...out, ...result.next.map((n ) => `next: ${n}`)];
|
|
148
148
|
}
|
|
149
149
|
if (result.action === 'settle') return [`${result.fp} settled as ${result.as}${result.followup ? `, follow-up ${result.followup.id}` : ''}`];
|
|
150
150
|
return [`capture ${result.effective}${result.sentinel ? ' (switched off on this machine)' : ''}${result.env ? ` (DOFLOW_FAILURE_CAPTURE=${result.env})` : ''}`];
|
|
@@ -157,17 +157,17 @@ function lines(result: any): string[] {
|
|
|
157
157
|
* @param {boolean} [options.all] `list`: every status, not just new and regressed
|
|
158
158
|
* @param {Object} [options.flags] fp, as, reason, set
|
|
159
159
|
*/
|
|
160
|
-
function handleFailureCommand({ action, cwd, json = false, all = false, flags = {} }
|
|
161
|
-
let result
|
|
160
|
+
function handleFailureCommand({ action, cwd, json = false, all = false, flags = {} } = {}) {
|
|
161
|
+
let result ;
|
|
162
162
|
try {
|
|
163
163
|
if (!ACTIONS.includes(action)) throw new FailureUsageError(`--action is required: one of ${ACTIONS.join(', ')}${action ? ` (got '${action}')` : ''}`);
|
|
164
164
|
if (action === 'list') result = listFailures({ all });
|
|
165
|
-
else if (action === 'settle') result = settleFailure({ fp: (flags
|
|
166
|
-
else result = captureState({ set: (flags
|
|
165
|
+
else if (action === 'settle') result = settleFailure({ fp: (flags ).fp, as: (flags ).as, reason: (flags ).reason, cwd });
|
|
166
|
+
else result = captureState({ set: (flags ).set });
|
|
167
167
|
} catch (error) {
|
|
168
|
-
if (error instanceof FailureUsageError || error instanceof StoreUnsafeError) return usageError('failure', (error
|
|
168
|
+
if (error instanceof FailureUsageError || error instanceof StoreUnsafeError) return usageError('failure', (error ).message, json);
|
|
169
169
|
if (!(error instanceof StoreLockedError)) throw error;
|
|
170
|
-
result = refusal(action, 'store-locked', (error
|
|
170
|
+
result = refusal(action, 'store-locked', (error ).message);
|
|
171
171
|
}
|
|
172
172
|
if (json) console.log(JSON.stringify(result, null, 2));
|
|
173
173
|
else if (result.ok === false) console.log(printSafe(`${result.finding}: ${result.message}`));
|
|
@@ -18,7 +18,7 @@ const OFF_VALUES = new Set(['off', '0', 'false', 'no']);
|
|
|
18
18
|
|
|
19
19
|
/** @param {Object} [env] defaults to process.env
|
|
20
20
|
* @returns {string|null} the failure home, an absolute path, or null when none can be resolved */
|
|
21
|
-
function failureHome(env
|
|
21
|
+
function failureHome(env = process.env) {
|
|
22
22
|
const xdg = env.XDG_CONFIG_HOME;
|
|
23
23
|
if (typeof xdg === 'string' && xdg !== '') {
|
|
24
24
|
return path.isAbsolute(xdg) ? path.join(xdg, 'doflow', 'failures') : null;
|
|
@@ -28,11 +28,11 @@ function failureHome(env: any = process.env): string | null {
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const sentinelPath = (home
|
|
32
|
-
const eventsPath = (home
|
|
31
|
+
const sentinelPath = (home ) => path.join(home, 'off');
|
|
32
|
+
const eventsPath = (home ) => path.join(home, 'events.jsonl');
|
|
33
33
|
|
|
34
34
|
/** The value of `DOFLOW_FAILURE_CAPTURE` when it is set to anything, else null. */
|
|
35
|
-
function envSetting(env
|
|
35
|
+
function envSetting(env = process.env) {
|
|
36
36
|
const raw = env.DOFLOW_FAILURE_CAPTURE;
|
|
37
37
|
return typeof raw === 'string' && raw !== '' ? raw : null;
|
|
38
38
|
}
|
|
@@ -44,7 +44,7 @@ function envSetting(env: any = process.env): string | null {
|
|
|
44
44
|
* @param {Object} [env]
|
|
45
45
|
* @returns {{effective: 'on'|'off', sentinel: boolean, env: string|null}}
|
|
46
46
|
*/
|
|
47
|
-
function captureSwitch(home
|
|
47
|
+
function captureSwitch(home , env = process.env) {
|
|
48
48
|
let sentinel = false;
|
|
49
49
|
if (home) {
|
|
50
50
|
try { sentinel = fs.statSync(sentinelPath(home)).isFile(); } catch { sentinel = false; }
|
|
@@ -55,7 +55,7 @@ function captureSwitch(home: string | null, env: any = process.env): { effective
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/** The hot-path check a writer makes: the environment first, then one `stat` of the sentinel. */
|
|
58
|
-
function captureIsOff(home
|
|
58
|
+
function captureIsOff(home , env = process.env) {
|
|
59
59
|
const setting = envSetting(env);
|
|
60
60
|
if (setting !== null && OFF_VALUES.has(setting.trim().toLowerCase())) return true;
|
|
61
61
|
try { return fs.statSync(sentinelPath(home)).isFile(); } catch { return false; }
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
const fs = require('node:fs');
|
|
19
19
|
const path = require('node:path');
|
|
20
20
|
const crypto = require('node:crypto');
|
|
21
|
-
const { failureHome, captureSwitch, eventsPath } = require('./home.
|
|
22
|
-
const { printSafe } = require('../mask.
|
|
21
|
+
const { failureHome, captureSwitch, eventsPath } = require('./home.js');
|
|
22
|
+
const { printSafe } = require('../mask.js');
|
|
23
23
|
|
|
24
24
|
const ROTATED = /^events-\d{8}T\d{6}Z-\d+\.jsonl$/;
|
|
25
25
|
const SETTLEMENTS = 'settlements.jsonl';
|
|
@@ -29,18 +29,18 @@ const STATUSES = ['new', 'regressed', 'noise', 'fixed', 'imported'];
|
|
|
29
29
|
const SHOWN_BY_DEFAULT = new Set(['new', 'regressed']);
|
|
30
30
|
const PROJECTS_KEPT = 5;
|
|
31
31
|
|
|
32
|
-
const settlementsPath = (home
|
|
32
|
+
const settlementsPath = (home ) => path.join(home, SETTLEMENTS);
|
|
33
33
|
|
|
34
34
|
/** @param {{source: string, command: string, kind: string, message: string, frame: string|null}} event */
|
|
35
|
-
function fingerprint(event
|
|
35
|
+
function fingerprint(event ) {
|
|
36
36
|
const text = [event.source, event.command, event.kind, event.message, event.frame || ''].join('\n');
|
|
37
37
|
return crypto.createHash('sha256').update(text).digest('hex').slice(0, 16);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/** One IC-011 line, or null. Only the fields a fold reads are checked. */
|
|
41
|
-
function asEvent(value
|
|
41
|
+
function asEvent(value ) {
|
|
42
42
|
if (!value || typeof value !== 'object' || value.v !== 1) return null;
|
|
43
|
-
const text = (x
|
|
43
|
+
const text = (x ) => typeof x === 'string';
|
|
44
44
|
if (!text(value.at) || !SOURCES.has(value.source) || !text(value.command) || !text(value.kind)) return null;
|
|
45
45
|
return {
|
|
46
46
|
at: value.at,
|
|
@@ -61,20 +61,20 @@ function asEvent(value: any): any {
|
|
|
61
61
|
* not a regular file (a FIFO would block the read, a device never ends, a symlink may leave the folder)
|
|
62
62
|
* or cannot be read is `unreadable`, and nothing is read from it.
|
|
63
63
|
*/
|
|
64
|
-
function readLines(file
|
|
64
|
+
function readLines(file , accept ) {
|
|
65
65
|
const c = fs.constants;
|
|
66
|
-
let text
|
|
67
|
-
let fd
|
|
66
|
+
let text ;
|
|
67
|
+
let fd ;
|
|
68
68
|
try {
|
|
69
69
|
fd = fs.openSync(file, c.O_RDONLY | (c.O_NOFOLLOW || 0) | (c.O_NONBLOCK || 0));
|
|
70
70
|
if (!fs.fstatSync(fd).isFile()) return { rows: [], skipped: 0, unreadable: true };
|
|
71
71
|
text = fs.readFileSync(fd, 'utf8');
|
|
72
|
-
} catch (error
|
|
72
|
+
} catch (error ) {
|
|
73
73
|
return { rows: [], skipped: 0, unreadable: error.code !== 'ENOENT' };
|
|
74
74
|
} finally {
|
|
75
75
|
if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* read already done */ } }
|
|
76
76
|
}
|
|
77
|
-
const rows
|
|
77
|
+
const rows = [];
|
|
78
78
|
let skipped = 0;
|
|
79
79
|
for (const line of text.split('\n')) {
|
|
80
80
|
if (line.trim() === '') continue;
|
|
@@ -90,13 +90,13 @@ function readLines(file: string, accept: (v: any) => any): { rows: any[]; skippe
|
|
|
90
90
|
* @param {string} home
|
|
91
91
|
* @returns {{events: Object[], skippedLines: number, unreadable: string[]}} `unreadable` names the files that were not read
|
|
92
92
|
*/
|
|
93
|
-
function readEvents(home
|
|
94
|
-
let rotated
|
|
95
|
-
try { rotated = fs.readdirSync(home).filter((name
|
|
96
|
-
const events
|
|
93
|
+
function readEvents(home ) {
|
|
94
|
+
let rotated = [];
|
|
95
|
+
try { rotated = fs.readdirSync(home).filter((name ) => ROTATED.test(name)).sort(); } catch { /* no folder yet */ }
|
|
96
|
+
const events = [];
|
|
97
97
|
let skippedLines = 0;
|
|
98
|
-
const unreadable
|
|
99
|
-
for (const file of [...rotated.map((name
|
|
98
|
+
const unreadable = [];
|
|
99
|
+
for (const file of [...rotated.map((name ) => path.join(home, name)), eventsPath(home)]) {
|
|
100
100
|
const read = readLines(file, asEvent);
|
|
101
101
|
events.push(...read.rows);
|
|
102
102
|
skippedLines += read.skipped;
|
|
@@ -105,20 +105,20 @@ function readEvents(home: string): { events: any[]; skippedLines: number; unread
|
|
|
105
105
|
return { events, skippedLines, unreadable };
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
function asSettlement(value
|
|
108
|
+
function asSettlement(value ) {
|
|
109
109
|
if (!value || typeof value !== 'object' || value.v !== 1) return null;
|
|
110
110
|
if (typeof value.at !== 'string' || !/^[0-9a-f]{16}$/.test(value.fp) || !SETTLE_AS.includes(value.as)) return null;
|
|
111
111
|
return { at: value.at, fp: value.fp, as: value.as, reason: typeof value.reason === 'string' ? value.reason : null, followup: typeof value.followup === 'string' ? value.followup : null };
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/** @returns {{settlements: Object[], skippedLines: number, unreadable: string[]}} in file order, which is time order */
|
|
115
|
-
function readSettlements(home
|
|
115
|
+
function readSettlements(home ) {
|
|
116
116
|
const read = readLines(settlementsPath(home), asSettlement);
|
|
117
117
|
return { settlements: read.rows, skippedLines: read.skipped, unreadable: read.unreadable ? [SETTLEMENTS] : [] };
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/** The IC-013 status of an entry from its latest settlement and the events seen after it. */
|
|
121
|
-
function statusOf(settlement
|
|
121
|
+
function statusOf(settlement , sinceSettlement ) {
|
|
122
122
|
if (!settlement) return 'new';
|
|
123
123
|
if (settlement.as === 'fixed') return sinceSettlement > 0 ? 'regressed' : 'fixed';
|
|
124
124
|
return settlement.as;
|
|
@@ -130,7 +130,7 @@ function statusOf(settlement: any, sinceSettlement: number): string {
|
|
|
130
130
|
* @param {Object[]} settlements
|
|
131
131
|
* @returns {Object[]}
|
|
132
132
|
*/
|
|
133
|
-
function foldEntries(events
|
|
133
|
+
function foldEntries(events , settlements ) {
|
|
134
134
|
const latest = new Map();
|
|
135
135
|
for (const s of settlements) latest.set(s.fp, s);
|
|
136
136
|
const groups = new Map();
|
|
@@ -139,14 +139,14 @@ function foldEntries(events: any[], settlements: any[]): any[] {
|
|
|
139
139
|
if (!groups.has(fp)) groups.set(fp, []);
|
|
140
140
|
groups.get(fp).push(event);
|
|
141
141
|
}
|
|
142
|
-
const entries
|
|
142
|
+
const entries = [];
|
|
143
143
|
for (const [fp, group] of groups) {
|
|
144
|
-
const ordered = (group
|
|
144
|
+
const ordered = (group ).map((event , index ) => ({ event, index })).sort((a , b ) => (a.event.at < b.event.at ? -1 : a.event.at > b.event.at ? 1 : a.index - b.index)).map((x ) => x.event);
|
|
145
145
|
const first = ordered[0];
|
|
146
146
|
const last = ordered[ordered.length - 1];
|
|
147
147
|
const settlement = latest.get(fp) || null;
|
|
148
|
-
const since = settlement ? ordered.filter((e
|
|
149
|
-
const projects
|
|
148
|
+
const since = settlement ? ordered.filter((e ) => e.at > settlement.at).length : ordered.length;
|
|
149
|
+
const projects = [];
|
|
150
150
|
for (let i = ordered.length - 1; i >= 0 && projects.length < PROJECTS_KEPT; i--) {
|
|
151
151
|
if (ordered[i].project && !projects.includes(ordered[i].project)) projects.push(ordered[i].project);
|
|
152
152
|
}
|
|
@@ -168,16 +168,16 @@ function foldEntries(events: any[], settlements: any[]): any[] {
|
|
|
168
168
|
message: first.message,
|
|
169
169
|
frame: first.frame,
|
|
170
170
|
projects,
|
|
171
|
-
harnesses: [...new Set(ordered.map((e
|
|
172
|
-
exitCodes: [...new Set(ordered.map((e
|
|
171
|
+
harnesses: [...new Set(ordered.map((e ) => e.harness))].sort(),
|
|
172
|
+
exitCodes: [...new Set(ordered.map((e ) => e.exit).filter((x ) => x !== null))].sort((a , b ) => a - b),
|
|
173
173
|
settlement: settlement && { as: settlement.as, at: settlement.at, reason: settlement.reason, followup: settlement.followup },
|
|
174
174
|
}));
|
|
175
175
|
}
|
|
176
|
-
return entries.sort((a
|
|
176
|
+
return entries.sort((a , b ) => (a.lastSeen < b.lastSeen ? 1 : a.lastSeen > b.lastSeen ? -1 : a.fp < b.fp ? -1 : 1));
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
/** The shape a listing shows (IC-019). */
|
|
180
|
-
function listedEntry(entry
|
|
180
|
+
function listedEntry(entry ) {
|
|
181
181
|
return {
|
|
182
182
|
fp: entry.fp, status: entry.status, count: entry.count, command: entry.command, kind: entry.kind,
|
|
183
183
|
message: entry.message, lastSeen: entry.lastSeen, lastVersion: entry.lastVersion,
|
|
@@ -190,7 +190,7 @@ function listedEntry(entry: any): any {
|
|
|
190
190
|
* @param {Object} [options.env] defaults to process.env
|
|
191
191
|
* @returns {{home: string|null, capture: 'on'|'off', entries: Object[], counts: Object, skippedLines: number, unreadable: string[]}}
|
|
192
192
|
*/
|
|
193
|
-
function loadEntries({ env = process.env } = {})
|
|
193
|
+
function loadEntries({ env = process.env } = {}) {
|
|
194
194
|
const home = failureHome(env);
|
|
195
195
|
const counts = Object.fromEntries(STATUSES.map((status) => [status, 0]));
|
|
196
196
|
// With no failure home nothing is captured, whatever the switch says.
|
|
@@ -207,7 +207,7 @@ function loadEntries({ env = process.env } = {}): any {
|
|
|
207
207
|
* @param {string} home
|
|
208
208
|
* @param {{fp: string, as: string, reason?: string|null, followup?: string|null, now?: Date}} settlement
|
|
209
209
|
*/
|
|
210
|
-
function appendSettlement(home
|
|
210
|
+
function appendSettlement(home , { fp, as, reason = null, followup = null, now = new Date() } ) {
|
|
211
211
|
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
212
212
|
const line = JSON.stringify({ v: 1, at: now.toISOString(), fp, as, reason, followup });
|
|
213
213
|
fs.appendFileSync(settlementsPath(home), `${line}\n`, { flag: 'a', mode: 0o600 });
|
|
@@ -216,7 +216,7 @@ function appendSettlement(home: string, { fp, as, reason = null, followup = null
|
|
|
216
216
|
const DOFLOW_PACKAGE = '@khoavu882/doflow';
|
|
217
217
|
|
|
218
218
|
/** Whether `root` is the DoFlow repository itself: its `package.json` is named `@khoavu882/doflow`. */
|
|
219
|
-
function isDoflowRepo(root
|
|
219
|
+
function isDoflowRepo(root ) {
|
|
220
220
|
try { return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).name === DOFLOW_PACKAGE; } catch { return false; }
|
|
221
221
|
}
|
|
222
222
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const { execFileSync } = require('node:child_process');
|
|
7
|
-
const { resolveBashHelper } = require('../helper/bash-helper.
|
|
7
|
+
const { resolveBashHelper } = require('../helper/bash-helper.js');
|
|
8
8
|
|
|
9
9
|
// `resolveActiveFeature` was moved here from `scaffold/generate.js` (which had it from
|
|
10
10
|
// bin/doflow.js) so the scaffold and the decision register ask the same resolver. The root comes
|
|
@@ -31,18 +31,18 @@ const PATHS_HELPER = resolveBashHelper('do-paths.sh');
|
|
|
31
31
|
* `paths` is the resolver's own parsed JSON (repo-root-relative artifact paths and has_* flags),
|
|
32
32
|
* so a caller needs no second resolver call.
|
|
33
33
|
*/
|
|
34
|
-
function resolveActiveFeature({ projectRoot, slug = null }
|
|
34
|
+
function resolveActiveFeature({ projectRoot, slug = null } ) {
|
|
35
35
|
const helper = resolveBashHelper('do-paths.sh');
|
|
36
36
|
if (!helper) {
|
|
37
37
|
return { error: 'resolver-missing', message: 'the feature resolver (do-paths.sh) is missing from this install' };
|
|
38
38
|
}
|
|
39
|
-
const args
|
|
39
|
+
const args = [helper, '--json', '--require', 'feature'];
|
|
40
40
|
if (slug) args.push(`--slug=${slug}`);
|
|
41
41
|
|
|
42
|
-
let stdout
|
|
42
|
+
let stdout ;
|
|
43
43
|
try {
|
|
44
44
|
stdout = execFileSync('bash', args, { cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
45
|
-
} catch (error
|
|
45
|
+
} catch (error ) {
|
|
46
46
|
// Exit 2 is the resolver's one non-zero path (no active feature, or an ambiguous one) and it
|
|
47
47
|
// still prints its error object on stdout — that object carries the hint the user needs, so
|
|
48
48
|
// it is read rather than replaced with a generic message. Anything else (no bash, NFR-003)
|
|
@@ -56,7 +56,7 @@ function resolveActiveFeature({ projectRoot, slug = null }: { projectRoot: strin
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
let paths
|
|
59
|
+
let paths ;
|
|
60
60
|
try {
|
|
61
61
|
paths = JSON.parse(stdout);
|
|
62
62
|
} catch {
|