@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
|
@@ -29,17 +29,17 @@
|
|
|
29
29
|
const fs = require('node:fs');
|
|
30
30
|
const path = require('node:path');
|
|
31
31
|
const { execFileSync } = require('node:child_process');
|
|
32
|
-
const { REPO_ROOT } = require('../../helper/repo-root.
|
|
33
|
-
const { resolveBashHelper } = require('../../helper/bash-helper.
|
|
32
|
+
const { REPO_ROOT } = require('../../helper/repo-root.js');
|
|
33
|
+
const { resolveBashHelper } = require('../../helper/bash-helper.js');
|
|
34
34
|
|
|
35
35
|
const MAX_BUFFER = 256 * 1024 * 1024;
|
|
36
36
|
|
|
37
37
|
/** Where `do-git-state.sh` is: the shared resolver (package copy first, else the one projected beside the runtime). */
|
|
38
|
-
function resolveGitStateHelper(repoRoot
|
|
38
|
+
function resolveGitStateHelper(repoRoot = REPO_ROOT, existsImpl = fs.existsSync) {
|
|
39
39
|
return resolveBashHelper('do-git-state.sh', repoRoot, existsImpl);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function git(root
|
|
42
|
+
function git(root , args ) {
|
|
43
43
|
return execFileSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: MAX_BUFFER, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -49,7 +49,7 @@ function git(root: string, args: string[]): string {
|
|
|
49
49
|
* @returns {{integration_ref:string|null, feature_prefixes:string[], release_tags:string[], base_tag:string|null}
|
|
50
50
|
* |{error:string}}
|
|
51
51
|
*/
|
|
52
|
-
function readGitFacts(root
|
|
52
|
+
function readGitFacts(root , runtimeRoot = REPO_ROOT) {
|
|
53
53
|
const helper = resolveGitStateHelper(runtimeRoot);
|
|
54
54
|
if (!helper) return { error: 'git-state-helper-missing' };
|
|
55
55
|
try {
|
|
@@ -57,20 +57,20 @@ function readGitFacts(root: string, runtimeRoot: string = REPO_ROOT): any {
|
|
|
57
57
|
const facts = JSON.parse(out);
|
|
58
58
|
if (facts.error) return { error: facts.error };
|
|
59
59
|
return facts;
|
|
60
|
-
} catch (error
|
|
60
|
+
} catch (error ) {
|
|
61
61
|
return { error: error.code === 'ENOENT' ? 'bash-not-found' : 'git-state-failed' };
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function escapeRegExp(text
|
|
65
|
+
function escapeRegExp(text ) { return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
66
66
|
|
|
67
67
|
/** The slug appears in the subject bounded by its ends or by characters outside `[a-z0-9-]`. */
|
|
68
|
-
function subjectNames(subject
|
|
68
|
+
function subjectNames(subject , slug ) {
|
|
69
69
|
return new RegExp(`(?:^|[^a-z0-9-])${escapeRegExp(slug)}(?:$|[^a-z0-9-])`).test(subject);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/** First-parent merges of `ref`, oldest first. */
|
|
73
|
-
function readMerges(root
|
|
73
|
+
function readMerges(root , ref ) {
|
|
74
74
|
const out = git(root, ['log', '--first-parent', '--merges', '--format=%H%x09%P%x09%ct%x09%s', ref]);
|
|
75
75
|
return out.split('\n').filter(Boolean).map((line) => {
|
|
76
76
|
const [sha, parents, ct, ...subject] = line.split('\t');
|
|
@@ -79,9 +79,9 @@ function readMerges(root: string, ref: string): Array<{ sha: string; parents: st
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/** Branch refs by short name (`feat/x`, `origin/feat/x`) with their tips; local branches first. */
|
|
82
|
-
function readBranchTips(root
|
|
82
|
+
function readBranchTips(root ) {
|
|
83
83
|
const out = git(root, ['for-each-ref', '--format=%(refname)%09%(objectname)', 'refs/heads', 'refs/remotes']);
|
|
84
|
-
const tips
|
|
84
|
+
const tips = [];
|
|
85
85
|
for (const line of out.split('\n').filter(Boolean)) {
|
|
86
86
|
const [refname, sha] = line.split('\t');
|
|
87
87
|
if (refname.startsWith('refs/heads/')) tips.push({ short: refname.slice('refs/heads/'.length), local: refname.slice('refs/heads/'.length), sha });
|
|
@@ -94,7 +94,7 @@ function readBranchTips(root: string): Array<{ short: string; local: string; sha
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/** The commit a tag names, peeled through any number of tag objects, or null when it is no commit. */
|
|
97
|
-
function peelTag(root
|
|
97
|
+
function peelTag(root , refname ) {
|
|
98
98
|
try { return git(root, ['rev-parse', '--verify', '--quiet', `${refname}^{commit}`]).trim() || null; } catch { return null; }
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -103,14 +103,14 @@ function peelTag(root: string, refname: string): string | null {
|
|
|
103
103
|
* `tag.sort`, as `git tag --contains` did), so the first containing tag is the one it would name.
|
|
104
104
|
* @returns {Array<{name:string, commit:string|null}>}
|
|
105
105
|
*/
|
|
106
|
-
function readTagCommits(root
|
|
106
|
+
function readTagCommits(root , wanted ) {
|
|
107
107
|
const out = git(root, ['tag', '--list', 'v*', '--format=%(refname)%09%(objecttype)%09%(objectname)%09%(*objecttype)%09%(*objectname)']);
|
|
108
|
-
const tags
|
|
108
|
+
const tags = [];
|
|
109
109
|
for (const line of out.split('\n').filter(Boolean)) {
|
|
110
110
|
const [refname, type, object, peeledType, peeled] = line.split('\t');
|
|
111
111
|
const name = refname.slice('refs/tags/'.length);
|
|
112
112
|
if (!wanted.has(name)) continue;
|
|
113
|
-
let commit
|
|
113
|
+
let commit = null;
|
|
114
114
|
if (type === 'commit') commit = object;
|
|
115
115
|
else if (type === 'tag' && peeledType === 'commit') commit = peeled;
|
|
116
116
|
else if (type === 'tag') commit = peelTag(root, refname); // a tag of a tag: rare, one call each
|
|
@@ -134,53 +134,53 @@ function readTagCommits(root: string, wanted: Set<string>): Array<{ name: string
|
|
|
134
134
|
* @param {string} pinned a full commit sha
|
|
135
135
|
* @param {string[]} [revs] further full commit shas whose history is read too
|
|
136
136
|
*/
|
|
137
|
-
function readHistory(root
|
|
137
|
+
function readHistory(root , pinned , revs = []) {
|
|
138
138
|
const input = `${[pinned, ...revs].join('\n')}\n`;
|
|
139
139
|
const out = execFileSync('git', ['rev-list', '--parents', '--topo-order', '--stdin'], { cwd: root, input, encoding: 'utf8', maxBuffer: MAX_BUFFER, stdio: ['pipe', 'pipe', 'ignore'] });
|
|
140
|
-
const parents = new Map
|
|
141
|
-
const order
|
|
140
|
+
const parents = new Map ();
|
|
141
|
+
const order = [];
|
|
142
142
|
for (const line of out.split('\n')) {
|
|
143
143
|
if (!line) continue;
|
|
144
144
|
const [sha, ...rest] = line.split(' ');
|
|
145
145
|
parents.set(sha, rest);
|
|
146
146
|
order.push(sha);
|
|
147
147
|
}
|
|
148
|
-
const chain
|
|
149
|
-
for (let sha = pinned; parents.has(sha); sha = (parents.get(sha)
|
|
148
|
+
const chain = [];
|
|
149
|
+
for (let sha = pinned; parents.has(sha); sha = (parents.get(sha) )[0]) chain.push(sha);
|
|
150
150
|
chain.reverse();
|
|
151
|
-
const chainIndex = new Map
|
|
151
|
+
const chainIndex = new Map (chain.map((sha, i) => [sha, i]));
|
|
152
152
|
|
|
153
|
-
const introducer = new Map
|
|
153
|
+
const introducer = new Map ();
|
|
154
154
|
for (const head of chain) {
|
|
155
|
-
const stack
|
|
155
|
+
const stack = [head];
|
|
156
156
|
while (stack.length) {
|
|
157
|
-
const sha = stack.pop()
|
|
157
|
+
const sha = stack.pop() ;
|
|
158
158
|
if (introducer.has(sha) || !parents.has(sha)) continue;
|
|
159
159
|
introducer.set(sha, head);
|
|
160
|
-
stack.push(...(parents.get(sha)
|
|
160
|
+
stack.push(...(parents.get(sha) ));
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
// --topo-order lists no parent before its children, so walking it backwards meets parents first.
|
|
165
|
-
const chainMax = new Map
|
|
165
|
+
const chainMax = new Map ();
|
|
166
166
|
for (let i = order.length - 1; i >= 0; i -= 1) {
|
|
167
167
|
const sha = order[i];
|
|
168
|
-
let max
|
|
169
|
-
if (max === -1) for (const parent of parents.get(sha)
|
|
168
|
+
let max = chainIndex.has(sha) ? (chainIndex.get(sha) ) : -1;
|
|
169
|
+
if (max === -1) for (const parent of parents.get(sha) ) max = Math.max(max, chainMax.get(parent) ?? -1);
|
|
170
170
|
chainMax.set(sha, max);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
const reached = new Map
|
|
174
|
-
function ancestorsOf(sha
|
|
173
|
+
const reached = new Map ();
|
|
174
|
+
function ancestorsOf(sha ) {
|
|
175
175
|
let seen = reached.get(sha);
|
|
176
176
|
if (seen) return seen;
|
|
177
|
-
seen = new Set
|
|
178
|
-
const stack
|
|
177
|
+
seen = new Set ();
|
|
178
|
+
const stack = [sha];
|
|
179
179
|
while (stack.length) {
|
|
180
|
-
const next = stack.pop()
|
|
180
|
+
const next = stack.pop() ;
|
|
181
181
|
if (seen.has(next) || !parents.has(next)) continue;
|
|
182
182
|
seen.add(next);
|
|
183
|
-
stack.push(...(parents.get(next)
|
|
183
|
+
stack.push(...(parents.get(next) ));
|
|
184
184
|
}
|
|
185
185
|
reached.set(sha, seen);
|
|
186
186
|
return seen;
|
|
@@ -188,9 +188,9 @@ function readHistory(root: string, pinned: string, revs: string[] = []): { intro
|
|
|
188
188
|
|
|
189
189
|
return {
|
|
190
190
|
/** The chain commit that brought `sha` into `pinned`'s history, or undefined when it is not in it. */
|
|
191
|
-
introducer: (sha
|
|
191
|
+
introducer: (sha ) => introducer.get(sha),
|
|
192
192
|
/** `a` is `b` or an ancestor of it; `a` must be in the read. */
|
|
193
|
-
isAncestor(a
|
|
193
|
+
isAncestor(a , b ) {
|
|
194
194
|
const index = chainIndex.get(a);
|
|
195
195
|
if (index !== undefined) return index <= (chainMax.get(b) ?? -1);
|
|
196
196
|
return ancestorsOf(b).has(a);
|
|
@@ -205,17 +205,17 @@ function readHistory(root: string, pinned: string, revs: string[] = []): { intro
|
|
|
205
205
|
* (a fast-forward, or a branch with no commits of its own) is its own introducer, so it never counts;
|
|
206
206
|
* a tip the merge holds but its first parent does not came in on a non-first parent.
|
|
207
207
|
*/
|
|
208
|
-
function introducingMerge(history
|
|
208
|
+
function introducingMerge(history , mergeBySha , tip , lowerBound ) {
|
|
209
209
|
const merge = mergeBySha.get(history().introducer(tip));
|
|
210
210
|
if (!merge || merge.sha === tip || merge.ct < lowerBound) return null;
|
|
211
211
|
return merge;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
/** Evidence of a merge of `slug` into the pinned ref, tried in IC-021's order, or null. */
|
|
215
|
-
function findEvidence({ slug, lowerBound, merges, mergeBySha, newestCt, history, tips, prefixes, confirmed }
|
|
216
|
-
const branchNames = new Set(prefixes.map((prefix
|
|
215
|
+
function findEvidence({ slug, lowerBound, merges, mergeBySha, newestCt, history, tips, prefixes, confirmed } ) {
|
|
216
|
+
const branchNames = new Set(prefixes.map((prefix ) => `${prefix}/${slug}`));
|
|
217
217
|
// No merge is recent enough for this feature: no branch can have one, so the graph need not be read for it.
|
|
218
|
-
const branchTips = newestCt < lowerBound ? [] : tips.filter((t
|
|
218
|
+
const branchTips = newestCt < lowerBound ? [] : tips.filter((t ) => branchNames.has(t.local));
|
|
219
219
|
for (const tip of branchTips) {
|
|
220
220
|
const merge = introducingMerge(history, mergeBySha, tip.sha, lowerBound);
|
|
221
221
|
if (merge) return { kind: 'branch', ref: tip.short, commit: merge.sha };
|
|
@@ -228,8 +228,8 @@ function findEvidence({ slug, lowerBound, merges, mergeBySha, newestCt, history,
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
/** A value computed on first use, so a call that needs no graph never reads one. */
|
|
231
|
-
function lazy
|
|
232
|
-
let value
|
|
231
|
+
function lazy (make ) {
|
|
232
|
+
let value ;
|
|
233
233
|
let made = false;
|
|
234
234
|
return () => {
|
|
235
235
|
if (!made) { value = make(); made = true; }
|
|
@@ -237,12 +237,12 @@ function lazy<T>(make: () => T): () => T {
|
|
|
237
237
|
};
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
const BUCKET
|
|
240
|
+
const BUCKET = { finished: 'finished', 'awaiting-release': 'awaitingRelease', 'in-progress': 'inProgress', unknown: 'unknown' };
|
|
241
241
|
|
|
242
|
-
function emptyBuckets()
|
|
242
|
+
function emptyBuckets() { return { finished: [], awaitingRelease: [], inProgress: [], unknown: [] }; }
|
|
243
243
|
|
|
244
244
|
/** Slugs grouped in the four buckets IC-007 and IC-022 show. */
|
|
245
|
-
function bucketize(statuses
|
|
245
|
+
function bucketize(statuses , slugs = Object.keys(statuses)) {
|
|
246
246
|
const buckets = emptyBuckets();
|
|
247
247
|
for (const slug of slugs) {
|
|
248
248
|
const entry = statuses[slug];
|
|
@@ -251,12 +251,12 @@ function bucketize(statuses: any, slugs: string[] = Object.keys(statuses)): Reco
|
|
|
251
251
|
return buckets;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
function result(releaseMode
|
|
254
|
+
function result(releaseMode , integrationRef , reason , statuses , notDetected = [], behind = 0) {
|
|
255
255
|
return { releaseMode, integrationRef, integrationBehind: behind, reason, statuses, features: bucketize(statuses), notDetected };
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
/** The one-line note a stale local integration branch earns (DEC-044), or null. */
|
|
259
|
-
function behindNote(ref
|
|
259
|
+
function behindNote(ref , behind ) {
|
|
260
260
|
return behind > 0
|
|
261
261
|
? `local ${ref} is ${behind} commit${behind === 1 ? '' : 's'} behind origin/${ref}; a merge that exists only on origin is not seen until you pull`
|
|
262
262
|
: null;
|
|
@@ -277,10 +277,10 @@ function behindNote(ref: string, behind: number): string | null {
|
|
|
277
277
|
* failure?:string (git, or the git-facts helper, failed while deriving: `git-state-failed`, `bash-not-found`,
|
|
278
278
|
* `git-state-helper-missing`; an environment fault, so every status is `unknown`)}}
|
|
279
279
|
*/
|
|
280
|
-
function deriveStatuses({ root, fold, facts = readGitFacts(root) }
|
|
280
|
+
function deriveStatuses({ root, fold, facts = readGitFacts(root) } ) {
|
|
281
281
|
const tracked = fold.features;
|
|
282
|
-
const unknown = (reason
|
|
283
|
-
Object.fromEntries(tracked.map((f
|
|
282
|
+
const unknown = (reason ) => result('unknown', facts.integration_ref ?? null, reason,
|
|
283
|
+
Object.fromEntries(tracked.map((f ) => [f.slug, { status: 'unknown', evidence: null, release: null }])));
|
|
284
284
|
if (facts.error) return { ...unknown(`git facts unavailable: ${facts.error}`), failure: facts.error };
|
|
285
285
|
if (!facts.integration_ref) return unknown('no integration ref resolves (develop, main, master, origin/HEAD)');
|
|
286
286
|
|
|
@@ -297,21 +297,21 @@ function deriveStatuses({ root, fold, facts = readGitFacts(root) }: { root: stri
|
|
|
297
297
|
// Pin the ref once: every query below names the same commit.
|
|
298
298
|
const pinned = git(root, ['rev-parse', '--verify', `${ref}^{commit}`]).trim();
|
|
299
299
|
const merges = readMerges(root, pinned);
|
|
300
|
-
const mergeBySha = new Map(merges.map((m)
|
|
300
|
+
const mergeBySha = new Map(merges.map((m) => [m.sha, m]));
|
|
301
301
|
const newestCt = merges.reduce((max, m) => Math.max(max, m.ct), -Infinity);
|
|
302
302
|
const tips = readBranchTips(root);
|
|
303
|
-
const mergedSlugs = new Set
|
|
304
|
-
const recorded = new Map
|
|
303
|
+
const mergedSlugs = new Set (fold.merged.map((m ) => m.slug));
|
|
304
|
+
const recorded = new Map ();
|
|
305
305
|
for (const release of fold.releases) for (const f of release.features) if (!recorded.has(f.slug)) recorded.set(f.slug, release.tag);
|
|
306
|
-
const recordedTags = new Set(fold.releases.map((r
|
|
307
|
-
const unrecordedTags = new Set
|
|
306
|
+
const recordedTags = new Set(fold.releases.map((r ) => r.tag));
|
|
307
|
+
const unrecordedTags = new Set (facts.release_tags.filter((tag ) => !recordedTags.has(tag)));
|
|
308
308
|
// Only a tag with no release record can finish a feature by containment (DEC-030), so only those are read.
|
|
309
309
|
const tags = lazy(() => (releaseMode === 'tagged' && unrecordedTags.size > 0 ? readTagCommits(root, unrecordedTags) : []));
|
|
310
|
-
const history = lazy(() => readHistory(root, pinned, tags().map((t) => t.commit).filter(Boolean)
|
|
310
|
+
const history = lazy(() => readHistory(root, pinned, tags().map((t) => t.commit).filter(Boolean) ));
|
|
311
311
|
|
|
312
|
-
const statuses
|
|
313
|
-
const evidenceCommits
|
|
314
|
-
const notDetected
|
|
312
|
+
const statuses = {};
|
|
313
|
+
const evidenceCommits = {};
|
|
314
|
+
const notDetected = [];
|
|
315
315
|
for (const feature of tracked) {
|
|
316
316
|
const lowerBound = Math.floor(Date.parse(feature.trackedAt) / 1000);
|
|
317
317
|
const evidence = findEvidence({
|
|
@@ -319,20 +319,20 @@ function deriveStatuses({ root, fold, facts = readGitFacts(root) }: { root: stri
|
|
|
319
319
|
});
|
|
320
320
|
const shown = evidence && { kind: evidence.kind, ref: evidence.ref };
|
|
321
321
|
evidenceCommits[feature.slug] = evidence ? evidence.commit : null;
|
|
322
|
-
let status
|
|
323
|
-
let release
|
|
322
|
+
let status ;
|
|
323
|
+
let release = null;
|
|
324
324
|
if (releaseMode === 'untagged') {
|
|
325
325
|
status = evidence ? 'finished' : 'in-progress';
|
|
326
326
|
} else if (recorded.has(feature.slug)) {
|
|
327
327
|
// A record finishes the feature by itself: no containment is looked for.
|
|
328
328
|
status = 'finished';
|
|
329
|
-
release = recorded.get(feature.slug)
|
|
329
|
+
release = recorded.get(feature.slug) ;
|
|
330
330
|
} else {
|
|
331
331
|
// DEC-030: a merge inside a `v*` tag that has no release record also finishes the feature. A
|
|
332
332
|
// tag with a record uses only that record, and a feature excluded from it is already outside it.
|
|
333
333
|
// The evidence commit is a first-parent merge of the pinned ref, so it is in the graph read.
|
|
334
334
|
const containing = evidence && evidence.commit && unrecordedTags.size > 0
|
|
335
|
-
? tags().filter((t) => t.commit && history().isAncestor(evidence.commit
|
|
335
|
+
? tags().filter((t) => t.commit && history().isAncestor(evidence.commit , t.commit )).map((t) => t.name)
|
|
336
336
|
: [];
|
|
337
337
|
if (containing.length > 0) { status = 'finished'; release = containing[0]; }
|
|
338
338
|
else status = evidence ? 'awaiting-release' : 'in-progress';
|
|
@@ -343,7 +343,7 @@ function deriveStatuses({ root, fold, facts = readGitFacts(root) }: { root: stri
|
|
|
343
343
|
// `integrationSha` and `evidenceCommits` are for the release preview, which must use the very commit
|
|
344
344
|
// pinned here (DEC-044) and the full merge commit an evidence names, not its 7-character `ref`.
|
|
345
345
|
return { ...result(releaseMode, ref, null, statuses, notDetected, behind), integrationSha: pinned, evidenceCommits };
|
|
346
|
-
} catch (error
|
|
346
|
+
} catch (error ) {
|
|
347
347
|
// A git failure is an environment fault, not a DoFlow defect: it is reported, never thrown.
|
|
348
348
|
// `failure` tells a caller git itself failed, which it reports (overview) or refuses on (release, merged).
|
|
349
349
|
return { ...unknown(`git could not answer (git-state-failed): ${String(error.message).split('\n')[0]}`), failure: 'git-state-failed' };
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
|
|
25
25
|
const nodeFs = require('node:fs');
|
|
26
26
|
const path = require('node:path');
|
|
27
|
-
const { acquireLock, tryAcquireLock } = require('../task-state.
|
|
27
|
+
const { acquireLock, tryAcquireLock } = require('../task-state.js');
|
|
28
28
|
const {
|
|
29
29
|
EVENTS_REL, LIFECYCLE_REL, JOURNAL_REL, lockTarget, isEventName, randomChars, readEvents, readJournal, StoreUnsafeError,
|
|
30
|
-
} = require('./event-store.
|
|
31
|
-
const { retentionWindow, selectExpired } = require('./retention.
|
|
30
|
+
} = require('./event-store.js');
|
|
31
|
+
const { retentionWindow, selectExpired } = require('./retention.js');
|
|
32
32
|
|
|
33
33
|
const LEGACY_LIFECYCLE_REL = path.join('agent-docs', 'lifecycle');
|
|
34
34
|
const LEGACY_EVENTS_REL = path.join(LEGACY_LIFECYCLE_REL, 'events');
|
|
@@ -36,30 +36,30 @@ const LEGACY_EVENTS_REL = path.join(LEGACY_LIFECYCLE_REL, 'events');
|
|
|
36
36
|
const MARKER_REL = path.join(LIFECYCLE_REL, 'migrated.json');
|
|
37
37
|
const MIGRATING_PREFIX = 'events.migrating-';
|
|
38
38
|
const NOTICE = 'note: the lifecycle store is now .doflow/state/lifecycle/events; agent-docs/lifecycle/ is no longer read and can be deleted';
|
|
39
|
-
const removedLine = (count
|
|
40
|
-
const invalidLine = (raw
|
|
39
|
+
const removedLine = (count , hours ) => `retention: removed ${count} event files older than ${hours} h`;
|
|
40
|
+
const invalidLine = (raw ) => `warning: DOFLOW_RETENTION_HOURS='${raw}' is not a positive whole number of hours; ignored, nothing removed`;
|
|
41
41
|
/** Bounds one pass, so its journal stays far below the reader's cap and its lock is held briefly. */
|
|
42
42
|
const MAX_PASS_FILES = 20000;
|
|
43
43
|
/** Unlinks between two refreshes of the lock's age. */
|
|
44
44
|
const REFRESH_EVERY = 500;
|
|
45
|
-
const unwritableLine = (code
|
|
46
|
-
const unlinkLine = (code
|
|
45
|
+
const unwritableLine = (code ) => `warning: retention skipped: .doflow/state/lifecycle cannot be written (${code}); nothing was hidden or removed`;
|
|
46
|
+
const unlinkLine = (code ) => `warning: retention could not remove every file (${code}); they stay hidden and the next lifecycle command finishes the removal`;
|
|
47
47
|
|
|
48
|
-
function lstatOrNull(fsImpl
|
|
48
|
+
function lstatOrNull(fsImpl , file ) {
|
|
49
49
|
try { return fsImpl.lstatSync(file); } catch { return null; }
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/** The names in a folder, or none when it is absent or not a folder. */
|
|
53
|
-
function namesIn(fsImpl
|
|
53
|
+
function namesIn(fsImpl , dir ) {
|
|
54
54
|
try {
|
|
55
55
|
return fsImpl.readdirSync(dir).sort();
|
|
56
|
-
} catch (error
|
|
56
|
+
} catch (error ) {
|
|
57
57
|
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return [];
|
|
58
58
|
throw error;
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
function refuseLinks(fsImpl
|
|
62
|
+
function refuseLinks(fsImpl , root , rels ) {
|
|
63
63
|
for (const rel of rels) {
|
|
64
64
|
const folder = path.join(root, rel);
|
|
65
65
|
const st = lstatOrNull(fsImpl, folder);
|
|
@@ -68,21 +68,21 @@ function refuseLinks(fsImpl: any, root: any, rels: any) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/** The event files of the old folder that a copy takes: event-named regular files, nothing else. */
|
|
71
|
-
function legacyEventFiles(fsImpl
|
|
71
|
+
function legacyEventFiles(fsImpl , root ) {
|
|
72
72
|
const dir = path.join(root, LEGACY_EVENTS_REL);
|
|
73
73
|
const st = lstatOrNull(fsImpl, dir);
|
|
74
74
|
if (!st || !st.isDirectory()) return [];
|
|
75
|
-
return namesIn(fsImpl, dir).filter((name
|
|
75
|
+
return namesIn(fsImpl, dir).filter((name ) => {
|
|
76
76
|
if (!isEventName(name)) return false;
|
|
77
77
|
const entry = lstatOrNull(fsImpl, path.join(dir, name));
|
|
78
78
|
return Boolean(entry && entry.isFile());
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const hasMarker = (fsImpl
|
|
83
|
-
const storeHasEvents = (fsImpl
|
|
82
|
+
const hasMarker = (fsImpl , root ) => lstatOrNull(fsImpl, path.join(root, MARKER_REL)) !== null;
|
|
83
|
+
const storeHasEvents = (fsImpl , root ) => namesIn(fsImpl, path.join(root, EVENTS_REL)).some(isEventName);
|
|
84
84
|
|
|
85
|
-
function removeQuietly(fsImpl
|
|
85
|
+
function removeQuietly(fsImpl , target ) {
|
|
86
86
|
try { fsImpl.rmSync(target, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -90,26 +90,26 @@ function removeQuietly(fsImpl: any, target: any) {
|
|
|
90
90
|
* Flushes one copied file. The copy takes the source's mode, so it is made writable by its owner
|
|
91
91
|
* first, like any file the store writes: Windows refuses to flush a read-only handle.
|
|
92
92
|
*/
|
|
93
|
-
function fsyncCopy(fsImpl
|
|
93
|
+
function fsyncCopy(fsImpl , file ) {
|
|
94
94
|
fsImpl.chmodSync(file, 0o644);
|
|
95
95
|
const fd = fsImpl.openSync(file, 'r+');
|
|
96
96
|
try { fsImpl.fsyncSync(fd); } finally { fsImpl.closeSync(fd); }
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/** Flushes a folder's entries where the platform lets a folder be opened. */
|
|
100
|
-
function fsyncFolder(fsImpl
|
|
100
|
+
function fsyncFolder(fsImpl , dir ) {
|
|
101
101
|
let fd;
|
|
102
102
|
try { fd = fsImpl.openSync(dir, 'r'); } catch { return; }
|
|
103
103
|
try { fsImpl.fsyncSync(fd); } finally { fsImpl.closeSync(fd); }
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
const migrationFailed = (code
|
|
106
|
+
const migrationFailed = (code ) => ({
|
|
107
107
|
ok: false,
|
|
108
108
|
finding: 'store-migration-failed',
|
|
109
109
|
message: `could not copy agent-docs/lifecycle/events to .doflow/state/lifecycle/events (${code}); the old folder is unchanged and the next lifecycle command retries. Nothing was written.`,
|
|
110
110
|
lines: [],
|
|
111
111
|
});
|
|
112
|
-
const legacyUnreadable = (code
|
|
112
|
+
const legacyUnreadable = (code ) => ({
|
|
113
113
|
ok: false,
|
|
114
114
|
finding: 'store-migration-failed',
|
|
115
115
|
message: `could not list agent-docs/lifecycle/events (${code}), so the old store cannot be copied to .doflow/state/lifecycle/events; make the folder readable, or delete it if its events are not needed, and the next lifecycle command retries. Nothing was written.`,
|
|
@@ -117,7 +117,7 @@ const legacyUnreadable = (code: any) => ({
|
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
/** Writes `text` to `file` by a flushed temp file in the same folder and a rename. */
|
|
120
|
-
function writeAtomic(fsImpl
|
|
120
|
+
function writeAtomic(fsImpl , file , text ) {
|
|
121
121
|
const dir = path.dirname(file);
|
|
122
122
|
const tmp = path.join(dir, `.${path.basename(file, '.json')}-${process.pid}-${randomChars(6)}.tmp`);
|
|
123
123
|
try {
|
|
@@ -129,7 +129,7 @@ function writeAtomic(fsImpl: any, file: any, text: any) {
|
|
|
129
129
|
fsImpl.closeSync(fd);
|
|
130
130
|
}
|
|
131
131
|
fsImpl.renameSync(tmp, file);
|
|
132
|
-
} catch (error
|
|
132
|
+
} catch (error ) {
|
|
133
133
|
try { fsImpl.unlinkSync(tmp); } catch { /* never written */ }
|
|
134
134
|
throw error;
|
|
135
135
|
}
|
|
@@ -137,7 +137,7 @@ function writeAtomic(fsImpl: any, file: any, text: any) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/** Records that the store is in place; false when it cannot be written, so a later verb tries again. */
|
|
140
|
-
function writeMarker(fsImpl
|
|
140
|
+
function writeMarker(fsImpl , root , copied , now ) {
|
|
141
141
|
try {
|
|
142
142
|
writeAtomic(fsImpl, path.join(root, MARKER_REL), `${JSON.stringify({ v: 1, from: 'agent-docs/lifecycle/events', copied, at: now.toISOString() })}\n`);
|
|
143
143
|
return true;
|
|
@@ -150,7 +150,7 @@ function writeMarker(fsImpl: any, root: any, copied: any, now: any) {
|
|
|
150
150
|
* The copy itself, under the store lock, of the event files the caller listed. Returns the number
|
|
151
151
|
* copied, or the `store-migration-failed` refusal.
|
|
152
152
|
*/
|
|
153
|
-
function copyLegacyStore(fsImpl
|
|
153
|
+
function copyLegacyStore(fsImpl , root , files ) {
|
|
154
154
|
const lifecycleDir = path.join(root, LIFECYCLE_REL);
|
|
155
155
|
const eventsDir = path.join(root, EVENTS_REL);
|
|
156
156
|
const legacyDir = path.join(root, LEGACY_EVENTS_REL);
|
|
@@ -176,26 +176,26 @@ function copyLegacyStore(fsImpl: any, root: any, files: any) {
|
|
|
176
176
|
}
|
|
177
177
|
try {
|
|
178
178
|
fsImpl.renameSync(tmp, eventsDir);
|
|
179
|
-
} catch (error
|
|
179
|
+
} catch (error ) {
|
|
180
180
|
if (error.code !== 'EEXIST' && error.code !== 'ENOTEMPTY') throw error;
|
|
181
181
|
removeQuietly(fsImpl, tmp); // another copier committed first
|
|
182
182
|
}
|
|
183
183
|
fsyncFolder(fsImpl, lifecycleDir);
|
|
184
184
|
return files.length;
|
|
185
|
-
} catch (error
|
|
185
|
+
} catch (error ) {
|
|
186
186
|
if (tmp) removeQuietly(fsImpl, tmp);
|
|
187
187
|
return migrationFailed(error.code || error.message);
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
/** The UTC time an event file name's id starts with. */
|
|
192
|
-
function idTime(name
|
|
192
|
+
function idTime(name ) {
|
|
193
193
|
const m = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(\d{3})Z-/.exec(name);
|
|
194
194
|
return m ? Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6], +m[7]) : NaN;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
/** Writes the retention journal, flushed to disk, with a new generation. */
|
|
198
|
-
function writeJournal(fsImpl
|
|
198
|
+
function writeJournal(fsImpl , root , pending ) {
|
|
199
199
|
writeAtomic(fsImpl, path.join(root, JOURNAL_REL), `${JSON.stringify({ v: 1, generation: randomChars(16), pending })}\n`);
|
|
200
200
|
}
|
|
201
201
|
|
|
@@ -203,13 +203,13 @@ function writeJournal(fsImpl: any, root: any, pending: any) {
|
|
|
203
203
|
* Keeps the held store lock from looking stale: another verb breaks a lock whose folder is older
|
|
204
204
|
* than the lock's stale timeout, so a long pass renews its folder's time as it goes.
|
|
205
205
|
*/
|
|
206
|
-
function refreshLock(fsImpl
|
|
206
|
+
function refreshLock(fsImpl , root ) {
|
|
207
207
|
const now = new Date();
|
|
208
208
|
try { fsImpl.utimesSync(`${lockTarget(root)}.lock`, now, now); } catch { /* the lock folder is ours; a failed touch only shortens its life */ }
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
/** Unlinks pending event files without clearing the journal until every hidden name is finished. */
|
|
212
|
-
function removePending(fsImpl
|
|
212
|
+
function removePending(fsImpl , root , names , onRemoved = () => {}, { maxFiles = Infinity, shouldStop } = {}) {
|
|
213
213
|
const dir = path.join(root, EVENTS_REL);
|
|
214
214
|
let removed = 0;
|
|
215
215
|
for (let index = 0; index < names.length; index += 1) {
|
|
@@ -217,14 +217,14 @@ function removePending(fsImpl: any, root: any, names: any, onRemoved = () => {},
|
|
|
217
217
|
const file = path.join(dir, names[index]);
|
|
218
218
|
if (removed >= maxFiles) {
|
|
219
219
|
try { fsImpl.lstatSync(file); return { complete: false, removed }; }
|
|
220
|
-
catch (error
|
|
220
|
+
catch (error ) { if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error; continue; }
|
|
221
221
|
}
|
|
222
222
|
if (index % REFRESH_EVERY === 0) refreshLock(fsImpl, root);
|
|
223
223
|
try {
|
|
224
224
|
fsImpl.unlinkSync(file);
|
|
225
225
|
removed += 1;
|
|
226
226
|
onRemoved();
|
|
227
|
-
} catch (error
|
|
227
|
+
} catch (error ) {
|
|
228
228
|
if (error.code !== 'ENOENT') throw error;
|
|
229
229
|
}
|
|
230
230
|
}
|
|
@@ -233,7 +233,7 @@ function removePending(fsImpl: any, root: any, names: any, onRemoved = () => {},
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
/** Selects whole units only; a unit that does not fit waits for the next pass. */
|
|
236
|
-
function passFiles(units
|
|
236
|
+
function passFiles(units , { maxFiles = MAX_PASS_FILES, shouldStop } = {}) {
|
|
237
237
|
const files = [], limit = Math.max(0, Math.min(MAX_PASS_FILES, Number.isFinite(maxFiles) ? maxFiles : MAX_PASS_FILES));
|
|
238
238
|
let partial = false;
|
|
239
239
|
for (const unit of units) {
|
|
@@ -248,9 +248,9 @@ function passFiles(units: any, { maxFiles = MAX_PASS_FILES, shouldStop }: any =
|
|
|
248
248
|
* The retention pass, under the store lock: finish a pending journal within the caller's remaining
|
|
249
249
|
* limits, then remove whole eligible units. A stopped deletion keeps the complete name set hidden.
|
|
250
250
|
*/
|
|
251
|
-
function retain(fsImpl
|
|
251
|
+
function retain(fsImpl , root , window , now , { maxFiles = MAX_PASS_FILES, shouldStop } = {}) {
|
|
252
252
|
let removedCount = 0;
|
|
253
|
-
const selected
|
|
253
|
+
const selected = [];
|
|
254
254
|
try {
|
|
255
255
|
if (shouldStop?.()) return { removed: [], removedCount, warning: [], partial: true, selected };
|
|
256
256
|
const pending = readJournal(root, fsImpl).pending;
|
|
@@ -274,7 +274,7 @@ function retain(fsImpl: any, root: any, window: any, now: any, { maxFiles = MAX_
|
|
|
274
274
|
if (shouldStop?.()) return { removed: [], removedCount, warning: [], partial: true, selected };
|
|
275
275
|
const file = path.join(root, EVENTS_REL, name);
|
|
276
276
|
let info;
|
|
277
|
-
try { info = fsImpl.lstatSync(file); } catch (error
|
|
277
|
+
try { info = fsImpl.lstatSync(file); } catch (error ) { if (error.code !== 'ENOENT') throw error; }
|
|
278
278
|
selected.push({ name, bytes: info?.isFile() ? info.size : 0 });
|
|
279
279
|
}
|
|
280
280
|
refreshLock(fsImpl, root);
|
|
@@ -285,7 +285,7 @@ function retain(fsImpl: any, root: any, window: any, now: any, { maxFiles = MAX_
|
|
|
285
285
|
removed: window.source === 'default' ? [] : [removedLine(bounded.files.length, window.hours)],
|
|
286
286
|
removedCount, warning: [], partial: bounded.partial || !result.complete, selected,
|
|
287
287
|
};
|
|
288
|
-
} catch (error
|
|
288
|
+
} catch (error ) {
|
|
289
289
|
// A failure is not a stopped pass: `partial` means only a stop, a held lock or the removal cap,
|
|
290
290
|
// so throttled upkeep advances its due time instead of retrying a failing pass on every verb.
|
|
291
291
|
return { removed: [], removedCount, warning: [unlinkLine(error.code || error.message)], partial: false, selected, code: error.code || 'EIO' };
|
|
@@ -293,10 +293,10 @@ function retain(fsImpl: any, root: any, window: any, now: any, { maxFiles = MAX_
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
/** Runs a bounded lifecycle retention pass under a non-waiting store lock for the storage engine. */
|
|
296
|
-
function retainLocked(root
|
|
296
|
+
function retainLocked(root , options = {}) {
|
|
297
297
|
const { window, now = new Date(), fsImpl = nodeFs, maxFiles = MAX_PASS_FILES, shouldStop } = options;
|
|
298
298
|
const bounded = Object.hasOwn(options, 'maxFiles') || Object.hasOwn(options, 'shouldStop');
|
|
299
|
-
const outcome = (removed
|
|
299
|
+
const outcome = (removed , warning , partial = false, selected = [], code ) => bounded
|
|
300
300
|
? { removed, warning, partial, selected, ...(code ? { code } : {}) }
|
|
301
301
|
: { removed, warning };
|
|
302
302
|
const eventsDir = path.join(root, EVENTS_REL);
|
|
@@ -314,10 +314,10 @@ function retainLocked(root: any, options: any = {}) {
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
/** Whether a lock-free look finds anything for the retention pass to remove. */
|
|
317
|
-
function needsPrune(fsImpl
|
|
317
|
+
function needsPrune(fsImpl , root , window , now ) {
|
|
318
318
|
if (window.state !== 'on') return false;
|
|
319
319
|
const cutoff = now.getTime() - window.ms;
|
|
320
|
-
if (!namesIn(fsImpl, path.join(root, EVENTS_REL)).some((name
|
|
320
|
+
if (!namesIn(fsImpl, path.join(root, EVENTS_REL)).some((name ) => isEventName(name) && idTime(name) < cutoff)) return false;
|
|
321
321
|
return selectExpired(readEvents(root, { fsImpl }).events, { now, windowMs: window.ms }).files.length > 0;
|
|
322
322
|
}
|
|
323
323
|
|
|
@@ -330,7 +330,7 @@ function needsPrune(fsImpl: any, root: any, window: any, now: any) {
|
|
|
330
330
|
* `lines` are for stderr, without the verb prefix.
|
|
331
331
|
* @throws {StoreUnsafeError} a symlinked new store folder, or a symlinked old folder when a copy must read it
|
|
332
332
|
*/
|
|
333
|
-
function prepareStore(root
|
|
333
|
+
function prepareStore(root , { env = process.env, now = new Date(), fsImpl = nodeFs, registry } = {}) {
|
|
334
334
|
refuseLinks(fsImpl, root, [LIFECYCLE_REL, EVENTS_REL]);
|
|
335
335
|
const legacyDir = lstatOrNull(fsImpl, path.join(root, LEGACY_EVENTS_REL));
|
|
336
336
|
const notice = legacyDir && legacyDir.isDirectory() ? [NOTICE] : [];
|
|
@@ -342,7 +342,7 @@ function prepareStore(root: string, { env = process.env, now = new Date(), fsImp
|
|
|
342
342
|
if (storeHasEvents(fsImpl, root)) {
|
|
343
343
|
markerDue = true;
|
|
344
344
|
} else {
|
|
345
|
-
try { migrate = legacyEventFiles(fsImpl, root).length > 0; } catch (error
|
|
345
|
+
try { migrate = legacyEventFiles(fsImpl, root).length > 0; } catch (error ) { return legacyUnreadable(error.code || error.message); }
|
|
346
346
|
}
|
|
347
347
|
}
|
|
348
348
|
const retentionDue = readJournal(root, fsImpl).pending.length > 0 || needsPrune(fsImpl, root, window, now);
|
|
@@ -354,7 +354,7 @@ function prepareStore(root: string, { env = process.env, now = new Date(), fsImp
|
|
|
354
354
|
fsImpl.mkdirSync(lifecycleDir, { recursive: true });
|
|
355
355
|
// An unwritable folder would only show as a lock that never comes; name it for what it is.
|
|
356
356
|
fsImpl.accessSync(lifecycleDir, nodeFs.constants.W_OK);
|
|
357
|
-
} catch (error
|
|
357
|
+
} catch (error ) {
|
|
358
358
|
// Without a copy to make, an unwritable store only stops the retention pass, not the verb.
|
|
359
359
|
if (!migrate) return { ok: true, lines: [...notice, ...invalid, ...(retentionDue ? [unwritableLine(error.code || error.message)] : [])] };
|
|
360
360
|
return migrationFailed(error.code || error.message);
|
|
@@ -362,7 +362,7 @@ function prepareStore(root: string, { env = process.env, now = new Date(), fsImp
|
|
|
362
362
|
let release;
|
|
363
363
|
try {
|
|
364
364
|
release = acquireLock(fsImpl, lockTarget(root));
|
|
365
|
-
} catch (error
|
|
365
|
+
} catch (error ) {
|
|
366
366
|
if (/^Could not lock/.test(error.message || '')) return { ok: false, finding: 'store-locked', message: `${error.message} Nothing was written.`, lines: [] };
|
|
367
367
|
throw error;
|
|
368
368
|
}
|
|
@@ -373,7 +373,7 @@ function prepareStore(root: string, { env = process.env, now = new Date(), fsImp
|
|
|
373
373
|
marked = writeMarker(fsImpl, root, 0, now);
|
|
374
374
|
} else if (!marked) {
|
|
375
375
|
let files;
|
|
376
|
-
try { files = legacyEventFiles(fsImpl, root); } catch (error
|
|
376
|
+
try { files = legacyEventFiles(fsImpl, root); } catch (error ) { return legacyUnreadable(error.code || error.message); }
|
|
377
377
|
if (files.length) {
|
|
378
378
|
refuseLinks(fsImpl, root, [LEGACY_LIFECYCLE_REL, LEGACY_EVENTS_REL]);
|
|
379
379
|
const copied = copyLegacyStore(fsImpl, root, files);
|