@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
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* repository itself uses (for test stubs, which have no file extension of their own to read).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
const { detectCommands } = require('../command-detect.
|
|
13
|
+
const { detectCommands } = require('../command-detect.js');
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Per-file language comes from the file's own extension, because `plan.md` already states it:
|
|
@@ -22,7 +22,7 @@ const { detectCommands } = require('../command-detect.ts');
|
|
|
22
22
|
* is this repository" — is `command-detect.ts`'s, so it is asked there rather than answered a
|
|
23
23
|
* second time here. See `repoLanguage()` for the one place its output is interpreted.
|
|
24
24
|
*/
|
|
25
|
-
const EXTENSION_LANGUAGE
|
|
25
|
+
const EXTENSION_LANGUAGE = Object.freeze({
|
|
26
26
|
'.js': 'javascript',
|
|
27
27
|
'.mjs': 'javascript',
|
|
28
28
|
'.cjs': 'javascript',
|
|
@@ -41,7 +41,7 @@ const EXTENSION_LANGUAGE: Readonly<Record<string, string>> = Object.freeze({
|
|
|
41
41
|
* generator — there is no signature to emit for a markdown file — so these skip benignly and never
|
|
42
42
|
* push the run to a non-zero exit.
|
|
43
43
|
*/
|
|
44
|
-
const PROSE_EXTENSIONS
|
|
44
|
+
const PROSE_EXTENSIONS = new Set([
|
|
45
45
|
'.md', '.markdown', '.txt', '.rst',
|
|
46
46
|
'.yaml', '.yml', '.json', '.jsonc', '.toml', '.ini', '.cfg', '.conf', '.env', '.lock',
|
|
47
47
|
'.html', '.css', '.scss', '.svg', '.png', '.jpg', '.gif', '.csv',
|
|
@@ -53,7 +53,7 @@ const PROSE_EXTENSIONS: ReadonlySet<string> = new Set([
|
|
|
53
53
|
* plan that touches one produces "no emitter for Java", not silence — the gap is real and belongs
|
|
54
54
|
* in the manifest, not in a reader's assumptions.
|
|
55
55
|
*/
|
|
56
|
-
const UNEMITTED_SOURCE_EXTENSIONS
|
|
56
|
+
const UNEMITTED_SOURCE_EXTENSIONS = Object.freeze({
|
|
57
57
|
'.java': { name: 'Java', inProseGrammar: true },
|
|
58
58
|
'.kt': { name: 'Kotlin', inProseGrammar: true },
|
|
59
59
|
'.kts': { name: 'Kotlin', inProseGrammar: true },
|
|
@@ -73,7 +73,7 @@ const UNEMITTED_SOURCE_EXTENSIONS: Readonly<Record<string, { name: string; inPro
|
|
|
73
73
|
|
|
74
74
|
/** Splits an identifier-ish path segment into lowercase words, so every language can re-case it
|
|
75
75
|
* to its own convention from one parse. */
|
|
76
|
-
function words(name
|
|
76
|
+
function words(name ) {
|
|
77
77
|
return name
|
|
78
78
|
.replace(/\.[^.]+$/, '')
|
|
79
79
|
.split(/[^A-Za-z0-9]+/)
|
|
@@ -82,9 +82,9 @@ function words(name: string): string[] {
|
|
|
82
82
|
.map((part) => part.toLowerCase());
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
const camel = (parts
|
|
86
|
-
const pascal = (parts
|
|
87
|
-
const snake = (parts
|
|
85
|
+
const camel = (parts ) => parts.map((w, i) => (i === 0 ? w : w[0].toUpperCase() + w.slice(1))).join('') || 'scaffold';
|
|
86
|
+
const pascal = (parts ) => parts.map((w) => w[0].toUpperCase() + w.slice(1)).join('') || 'Scaffold';
|
|
87
|
+
const snake = (parts ) => parts.join('_') || 'scaffold';
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
90
|
* One entry per language this module can emit. Each `module` and `test` body is a signature plus a
|
|
@@ -92,10 +92,10 @@ const snake = (parts: string[]): string => parts.join('_') || 'scaffold';
|
|
|
92
92
|
* `references/scaffold.md` — the same table the external-dependency case uses, so the two halves of
|
|
93
93
|
* that document agree on what an unimplemented body looks like.
|
|
94
94
|
*/
|
|
95
|
-
const EMITTERS
|
|
95
|
+
const EMITTERS = Object.freeze({
|
|
96
96
|
javascript: {
|
|
97
|
-
line: (t
|
|
98
|
-
module: ({ symbol, message }
|
|
97
|
+
line: (t ) => (t ? `// ${t}` : '//'),
|
|
98
|
+
module: ({ symbol, message } ) => [
|
|
99
99
|
'/**',
|
|
100
100
|
' * Placeholder signature — replace with the real surface before implementing.',
|
|
101
101
|
' * @returns {never}',
|
|
@@ -106,11 +106,11 @@ const EMITTERS: Readonly<Record<string, any>> = Object.freeze({
|
|
|
106
106
|
'',
|
|
107
107
|
`module.exports = { ${symbol} };`,
|
|
108
108
|
].join('\n'),
|
|
109
|
-
testName: (id
|
|
110
|
-
test: ({ id, cases, message }
|
|
109
|
+
testName: (id ) => `${id}.test.js`,
|
|
110
|
+
test: ({ id, cases, message } ) => [
|
|
111
111
|
"const { test } = require('node:test');",
|
|
112
112
|
'',
|
|
113
|
-
...cases.map(({ statement }
|
|
113
|
+
...cases.map(({ statement } ) => [
|
|
114
114
|
`test(${JSON.stringify(`${id} — ${statement}`)}, () => {`,
|
|
115
115
|
` throw new Error(${JSON.stringify(message)});`,
|
|
116
116
|
'});',
|
|
@@ -120,18 +120,18 @@ const EMITTERS: Readonly<Record<string, any>> = Object.freeze({
|
|
|
120
120
|
},
|
|
121
121
|
|
|
122
122
|
typescript: {
|
|
123
|
-
line: (t
|
|
124
|
-
module: ({ symbol, message }
|
|
123
|
+
line: (t ) => (t ? `// ${t}` : '//'),
|
|
124
|
+
module: ({ symbol, message } ) => [
|
|
125
125
|
'/** Placeholder signature — replace with the real surface before implementing. */',
|
|
126
126
|
`export function ${symbol}(): never {`,
|
|
127
127
|
` throw new Error(${JSON.stringify(message)});`,
|
|
128
128
|
'}',
|
|
129
129
|
].join('\n'),
|
|
130
|
-
testName: (id
|
|
131
|
-
test: ({ id, cases, message }
|
|
130
|
+
testName: (id ) => `${id}.test.ts`,
|
|
131
|
+
test: ({ id, cases, message } ) => [
|
|
132
132
|
"import { test } from 'node:test';",
|
|
133
133
|
'',
|
|
134
|
-
...cases.map(({ statement }
|
|
134
|
+
...cases.map(({ statement } ) => [
|
|
135
135
|
`test(${JSON.stringify(`${id} — ${statement}`)}, (): void => {`,
|
|
136
136
|
` throw new Error(${JSON.stringify(message)});`,
|
|
137
137
|
'});',
|
|
@@ -141,14 +141,14 @@ const EMITTERS: Readonly<Record<string, any>> = Object.freeze({
|
|
|
141
141
|
},
|
|
142
142
|
|
|
143
143
|
python: {
|
|
144
|
-
line: (t
|
|
145
|
-
module: ({ symbol, message }
|
|
144
|
+
line: (t ) => (t ? `# ${t}` : '#'),
|
|
145
|
+
module: ({ symbol, message } ) => [
|
|
146
146
|
`def ${symbol}() -> None:`,
|
|
147
147
|
' """Placeholder signature — replace with the real surface before implementing."""',
|
|
148
148
|
` raise NotImplementedError(${JSON.stringify(message)})`,
|
|
149
149
|
].join('\n'),
|
|
150
|
-
testName: (id
|
|
151
|
-
test: ({ cases, message }
|
|
150
|
+
testName: (id ) => `test_${id.toLowerCase().replace(/-/g, '_')}.py`,
|
|
151
|
+
test: ({ cases, message } ) => cases.map(({ statement } , i ) => [
|
|
152
152
|
`def test_case_${i + 1}() -> None:`,
|
|
153
153
|
` """${statement.replace(/"/g, "'")}"""`,
|
|
154
154
|
` raise NotImplementedError(${JSON.stringify(message)})`,
|
|
@@ -157,8 +157,8 @@ const EMITTERS: Readonly<Record<string, any>> = Object.freeze({
|
|
|
157
157
|
},
|
|
158
158
|
|
|
159
159
|
go: {
|
|
160
|
-
line: (t
|
|
161
|
-
module: ({ symbol, message, pkg }
|
|
160
|
+
line: (t ) => (t ? `// ${t}` : '//'),
|
|
161
|
+
module: ({ symbol, message, pkg } ) => [
|
|
162
162
|
`package ${pkg}`,
|
|
163
163
|
'',
|
|
164
164
|
'import "errors"',
|
|
@@ -168,13 +168,13 @@ const EMITTERS: Readonly<Record<string, any>> = Object.freeze({
|
|
|
168
168
|
`\treturn errors.New(${JSON.stringify(message)})`,
|
|
169
169
|
'}',
|
|
170
170
|
].join('\n'),
|
|
171
|
-
testName: (id
|
|
172
|
-
test: ({ id, cases, message, pkg }
|
|
171
|
+
testName: (id ) => `${id.toLowerCase().replace(/-/g, '_')}_test.go`,
|
|
172
|
+
test: ({ id, cases, message, pkg } ) => [
|
|
173
173
|
`package ${pkg}`,
|
|
174
174
|
'',
|
|
175
175
|
'import "testing"',
|
|
176
176
|
'',
|
|
177
|
-
...cases.map(({ statement }
|
|
177
|
+
...cases.map(({ statement } , i ) => [
|
|
178
178
|
`// ${statement}`,
|
|
179
179
|
`func Test${pascal(words(id))}Case${i + 1}(t *testing.T) {`,
|
|
180
180
|
`\tt.Fatal(${JSON.stringify(message)})`,
|
|
@@ -185,15 +185,15 @@ const EMITTERS: Readonly<Record<string, any>> = Object.freeze({
|
|
|
185
185
|
},
|
|
186
186
|
|
|
187
187
|
rust: {
|
|
188
|
-
line: (t
|
|
189
|
-
module: ({ symbol, message }
|
|
188
|
+
line: (t ) => (t ? `// ${t}` : '//'),
|
|
189
|
+
module: ({ symbol, message } ) => [
|
|
190
190
|
'/// Placeholder signature — replace with the real surface before implementing.',
|
|
191
191
|
`pub fn ${symbol}() {`,
|
|
192
192
|
` unimplemented!(${JSON.stringify(message)});`,
|
|
193
193
|
'}',
|
|
194
194
|
].join('\n'),
|
|
195
|
-
testName: (id
|
|
196
|
-
test: ({ id, cases, message }
|
|
195
|
+
testName: (id ) => `${id.toLowerCase().replace(/-/g, '_')}.rs`,
|
|
196
|
+
test: ({ id, cases, message } ) => cases.map(({ statement } , i ) => [
|
|
197
197
|
`// ${statement}`,
|
|
198
198
|
'#[test]',
|
|
199
199
|
`fn ${snake(words(id))}_case_${i + 1}() {`,
|
|
@@ -212,12 +212,12 @@ const EMITTERS: Readonly<Record<string, any>> = Object.freeze({
|
|
|
212
212
|
* TypeScript. The one extra bit is read from its own output (a `typecheck` role exists only when a
|
|
213
213
|
* tsc script or a `tsconfig.json` was found), so no second detector is introduced.
|
|
214
214
|
*/
|
|
215
|
-
function repoLanguage(repoRoot
|
|
215
|
+
function repoLanguage(repoRoot , fsImpl ) {
|
|
216
216
|
const detected = detectCommands({ projectRoot: repoRoot, fsImpl });
|
|
217
217
|
if (!detected.manifestFound) {
|
|
218
218
|
return { id: null, signal: 'none', why: 'no build manifest found by command-detect.ts' };
|
|
219
219
|
}
|
|
220
|
-
const has = (name
|
|
220
|
+
const has = (name ) => detected.manifests.includes(name);
|
|
221
221
|
if (has('package.json')) {
|
|
222
222
|
const typescript = Boolean(detected.commands.typecheck);
|
|
223
223
|
return { id: typescript ? 'typescript' : 'javascript', signal: 'package.json', why: null };
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const fs = require('node:fs');
|
|
7
|
-
const { loadStorageRegistry } = require('./registry.
|
|
8
|
-
const { resolveRoots, readPruneSetting } = require('./roots.
|
|
9
|
-
const { runPass } = require('./engine.
|
|
10
|
-
const { upkeepAfterVerb } = require('./upkeep.
|
|
11
|
-
const { REPO_ROOT } = require('../../helper/repo-root.
|
|
7
|
+
const { loadStorageRegistry } = require('./registry.js');
|
|
8
|
+
const { resolveRoots, readPruneSetting } = require('./roots.js');
|
|
9
|
+
const { runPass } = require('./engine.js');
|
|
10
|
+
const { upkeepAfterVerb } = require('./upkeep.js');
|
|
11
|
+
const { REPO_ROOT } = require('../../helper/repo-root.js');
|
|
12
12
|
|
|
13
|
-
function handleStorageCommand({ action = 'report', json = false, global = false, projectRoot = '.', env = process.env, fsImpl = fs }
|
|
13
|
+
function handleStorageCommand({ action = 'report', json = false, global = false, projectRoot = '.', env = process.env, fsImpl = fs } = {}) {
|
|
14
14
|
if (!['report', 'prune', 'upkeep'].includes(action)) {
|
|
15
15
|
console.error(`doflow storage: unknown action '${action}' (expected report, prune, or upkeep)`);
|
|
16
16
|
process.exitCode = 2;
|
|
@@ -23,7 +23,7 @@ function handleStorageCommand({ action = 'report', json = false, global = false,
|
|
|
23
23
|
}
|
|
24
24
|
let registry;
|
|
25
25
|
try { registry = loadStorageRegistry({ repoRoot: REPO_ROOT, fsImpl }); }
|
|
26
|
-
catch (error
|
|
26
|
+
catch (error ) {
|
|
27
27
|
console.error(`doflow storage: storage registry unavailable${error.message ? `: ${error.message}` : ''}`);
|
|
28
28
|
process.exitCode = 2;
|
|
29
29
|
return;
|
|
@@ -35,7 +35,7 @@ function handleStorageCommand({ action = 'report', json = false, global = false,
|
|
|
35
35
|
readPruneSetting({ registry, roots, rootId, fsImpl })]));
|
|
36
36
|
const mode = action === 'prune' ? 'prune' : 'report';
|
|
37
37
|
const result = runPass({ registry, roots, mode, setting, now: Date.now(), env, fsImpl });
|
|
38
|
-
const removed = result.stores.reduce((sum
|
|
38
|
+
const removed = result.stores.reduce((sum , row ) => sum + row.removed, 0);
|
|
39
39
|
const entries = `entr${removed === 1 ? 'y' : 'ies'}`;
|
|
40
40
|
if (action === 'prune' && Object.values(setting).every(value => value.setting === 'off')) {
|
|
41
41
|
const decision = projectSetting.decidedBy || userSetting.decidedBy || path.join(roots['config-home'], registry.settings.optOutFile);
|
|
@@ -54,7 +54,7 @@ function handleStorageCommand({ action = 'report', json = false, global = false,
|
|
|
54
54
|
}
|
|
55
55
|
console.log(`Settings: project ${projectSetting.setting}${projectSetting.decidedBy ? ` (${projectSetting.decidedBy})` : ' (default)'}; user ${userSetting.setting}${userSetting.decidedBy ? ` (${userSetting.decidedBy})` : ' (default)'}`);
|
|
56
56
|
for (const row of result.stores) {
|
|
57
|
-
const names = row.selected.slice(0, 20).map((entry
|
|
57
|
+
const names = row.selected.slice(0, 20).map((entry ) => entry.name);
|
|
58
58
|
const more = row.selected.length > 20 ? `\n ... and ${row.selected.length - 20} more` : '';
|
|
59
59
|
console.log(`${row.id}\n Location: ${row.location || '(not configured)'}\n Entries: ${row.entries}; size: ${row.bytes} bytes\n Bound: ${row.bound}\n Would remove: ${row.selected.length}${names.length ? `\n ${names.join('\n ')}` : ''}${more}`);
|
|
60
60
|
}
|
|
@@ -5,26 +5,26 @@
|
|
|
5
5
|
const fs = require('node:fs');
|
|
6
6
|
const path = require('node:path');
|
|
7
7
|
const { randomBytes } = require('node:crypto');
|
|
8
|
-
const { formatBound } = require('./registry.
|
|
9
|
-
const { resolveStorePath } = require('./roots.
|
|
10
|
-
const { selectCandidates, recordStillCandidate, protectedLogs, removableRules, ruleOrder, projectRoot } = require('./rules.
|
|
11
|
-
const { retainLocked } = require('../lifecycle/store-upkeep.
|
|
12
|
-
const { parseWindow, selectExpired } = require('../lifecycle/retention.
|
|
13
|
-
const { readEvents } = require('../lifecycle/event-store.
|
|
14
|
-
const { createWorkCache } = require('./work-status.
|
|
15
|
-
const { projectKey } = require('../lifecycle/report-store.
|
|
8
|
+
const { formatBound } = require('./registry.js');
|
|
9
|
+
const { resolveStorePath } = require('./roots.js');
|
|
10
|
+
const { selectCandidates, recordStillCandidate, protectedLogs, removableRules, ruleOrder, projectRoot } = require('./rules.js');
|
|
11
|
+
const { retainLocked } = require('../lifecycle/store-upkeep.js');
|
|
12
|
+
const { parseWindow, selectExpired } = require('../lifecycle/retention.js');
|
|
13
|
+
const { readEvents } = require('../lifecycle/event-store.js');
|
|
14
|
+
const { createWorkCache } = require('./work-status.js');
|
|
15
|
+
const { projectKey } = require('../lifecycle/report-store.js');
|
|
16
16
|
const SAFE_NAMESPACE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
17
17
|
const TOMBSTONE = /^\.[A-Za-z0-9][A-Za-z0-9._-]*\.pruning-[0-9]+-[0-9a-f]{8}$/;
|
|
18
18
|
|
|
19
|
-
function stat(fsImpl
|
|
20
|
-
function ownedPathLinked(store
|
|
19
|
+
function stat(fsImpl ,file ) { try { return fsImpl.lstatSync(file); } catch { return null; } }
|
|
20
|
+
function ownedPathLinked(store ,roots ,fsImpl ) {
|
|
21
21
|
const root=roots[store.root]; if (!root) return false;
|
|
22
22
|
const owned=path.basename(root)==='doflow' || path.basename(root)==='.doflow' ? root : path.dirname(root);
|
|
23
|
-
for (const dir of [owned,...(store.path ? [root,...store.path.split('/').reduce((a
|
|
23
|
+
for (const dir of [owned,...(store.path ? [root,...store.path.split('/').reduce((a ,part )=>{a.push(path.join(a.at(-1)||root,part));return a;},[])] : [root])])
|
|
24
24
|
if (stat(fsImpl,dir)?.isSymbolicLink()) return true;
|
|
25
25
|
return false;
|
|
26
26
|
}
|
|
27
|
-
function inventory(store
|
|
27
|
+
function inventory(store ,storeDir ,fsImpl ,shouldStop ) {
|
|
28
28
|
const match=new RegExp(store.match), entries=[];
|
|
29
29
|
for (const name of fsImpl.readdirSync(storeDir)) {
|
|
30
30
|
if (shouldStop?.()) return null;
|
|
@@ -39,7 +39,7 @@ function inventory(store: any,storeDir: any,fsImpl: any,shouldStop: any) {
|
|
|
39
39
|
}
|
|
40
40
|
return entries;
|
|
41
41
|
}
|
|
42
|
-
function sweepTombstones(fsImpl
|
|
42
|
+
function sweepTombstones(fsImpl ,storeDir ,row ) {
|
|
43
43
|
for (const name of fsImpl.readdirSync(storeDir)) {
|
|
44
44
|
if (!TOMBSTONE.test(name)) continue;
|
|
45
45
|
const target=path.join(storeDir,name);
|
|
@@ -49,13 +49,13 @@ function sweepTombstones(fsImpl: any,storeDir: any,row: any) {
|
|
|
49
49
|
// Node's recursive rm removes nested links themselves, never their referents.
|
|
50
50
|
fsImpl.rmSync(target,{recursive:true});
|
|
51
51
|
row.removed++;
|
|
52
|
-
} catch (error
|
|
52
|
+
} catch (error ) {
|
|
53
53
|
if (error.code==='ENOENT') row.removed++;
|
|
54
54
|
else row.failed.push({name,code:error.code || 'EIO'});
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
function removalOrder(candidates
|
|
58
|
+
function removalOrder(candidates ) {
|
|
59
59
|
const oldest=[...candidates].sort((a,b)=>a.mtimeMs-b.mtimeMs || a.name.localeCompare(b.name));
|
|
60
60
|
const byName=new Map(oldest.map(candidate=>[candidate.name,candidate])), emitted=new Set(), ordered=[];
|
|
61
61
|
for (const candidate of oldest) {
|
|
@@ -67,9 +67,9 @@ function removalOrder(candidates: any) {
|
|
|
67
67
|
}
|
|
68
68
|
return ordered;
|
|
69
69
|
}
|
|
70
|
-
function removeOne(fsImpl
|
|
70
|
+
function removeOne(fsImpl ,storeDir ,candidate ,store ,context ) {
|
|
71
71
|
const parts=candidate.name.split('/');
|
|
72
|
-
if (parts.length>2 || parts.some((part
|
|
72
|
+
if (parts.length>2 || parts.some((part )=>!part || part==='.' || part==='..')) throw Object.assign(new Error('unsafe name'),{code:'EINVAL'});
|
|
73
73
|
const target=path.join(storeDir,...parts), lock=`${target}.lock`;
|
|
74
74
|
let locked=false;
|
|
75
75
|
try {
|
|
@@ -79,7 +79,7 @@ function removeOne(fsImpl: any,storeDir: any,candidate: any,store: any,context:
|
|
|
79
79
|
}
|
|
80
80
|
if (store.entry==='record') {
|
|
81
81
|
try { fsImpl.mkdirSync(lock); locked=true; }
|
|
82
|
-
catch(error
|
|
82
|
+
catch(error ) { if (error.code==='EEXIST') return false; throw error; }
|
|
83
83
|
}
|
|
84
84
|
const info=fsImpl.lstatSync(target);
|
|
85
85
|
if (info.isSymbolicLink()) return false;
|
|
@@ -104,14 +104,14 @@ function removeOne(fsImpl: any,storeDir: any,candidate: any,store: any,context:
|
|
|
104
104
|
} else if (info.isFile()) fsImpl.unlinkSync(target);
|
|
105
105
|
else return false;
|
|
106
106
|
return true;
|
|
107
|
-
} catch(error
|
|
107
|
+
} catch(error ) { if (error.code==='ENOENT') return true; throw error; }
|
|
108
108
|
finally {
|
|
109
109
|
if (locked) {
|
|
110
|
-
try { fsImpl.rmdirSync(lock); } catch(error
|
|
110
|
+
try { fsImpl.rmdirSync(lock); } catch(error ) { if (error.code!=='ENOENT') throw error; }
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
-
function cleanEmptyNamespace(fsImpl
|
|
114
|
+
function cleanEmptyNamespace(fsImpl ,storeDir ,name ,report = false) {
|
|
115
115
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) return;
|
|
116
116
|
const dir=path.join(storeDir,name);
|
|
117
117
|
try {
|
|
@@ -119,7 +119,7 @@ function cleanEmptyNamespace(fsImpl: any,storeDir: any,name: any,report = false)
|
|
|
119
119
|
fsImpl.rmdirSync(dir);
|
|
120
120
|
} catch { /* another record, or an active writer */ }
|
|
121
121
|
}
|
|
122
|
-
function projectKeyMarker(fsImpl
|
|
122
|
+
function projectKeyMarker(fsImpl ,storeDir ,roots ) {
|
|
123
123
|
const project=projectRoot(roots);
|
|
124
124
|
if (!project) return;
|
|
125
125
|
let key;
|
|
@@ -129,15 +129,15 @@ function projectKeyMarker(fsImpl: any,storeDir: any,roots: any) {
|
|
|
129
129
|
try { fsImpl.writeFileSync(path.join(dir,'.root'),`${fsImpl.realpathSync(project)}\n`,{flag:'wx'}); } catch { /* best effort */ }
|
|
130
130
|
}
|
|
131
131
|
/** A bounded pass over registry-declared stores; report mode never writes. */
|
|
132
|
-
function runPass({registry,roots,stores,mode,setting={},now=Date.now(),budgetMs,maxRemovals,env=process.env,fsImpl=fs}
|
|
132
|
+
function runPass({registry,roots,stores,mode,setting={},now=Date.now(),budgetMs,maxRemovals,env=process.env,fsImpl=fs} ) {
|
|
133
133
|
if (mode!=='report' && mode!=='prune') throw new Error('Storage pass mode must be report or prune');
|
|
134
134
|
const started=performance.now(), budget=budgetMs===undefined?(mode==='report'?Infinity:registry.settings.passBudgetMs):budgetMs;
|
|
135
135
|
const cap=maxRemovals===undefined?registry.settings.maxRemovalsPerPass:maxRemovals;
|
|
136
|
-
const requested=new Set(stores || registry.stores.filter((s
|
|
136
|
+
const requested=new Set(stores || registry.stores.filter((s )=>removableRules.has(s.rule)).map((s )=>s.id));
|
|
137
137
|
const ordered=[...registry.stores].filter(s=>requested.has(s.id) && removableRules.has(s.rule))
|
|
138
138
|
.sort((a,b)=>ruleOrder.indexOf(a.id)-ruleOrder.indexOf(b.id));
|
|
139
|
-
const result
|
|
140
|
-
let removals=0, protectedSet
|
|
139
|
+
const result ={mode,partial:false,durationMs:0,stores:[]};
|
|
140
|
+
let removals=0, protectedSet =null;
|
|
141
141
|
const shouldStop=()=>mode==='prune' && (removals>=cap || performance.now()-started>=budget);
|
|
142
142
|
const project=projectRoot(roots);
|
|
143
143
|
const workCache=project && ordered.some(s=>s.rule==='work' || s.protect==='open-work')
|
|
@@ -148,7 +148,7 @@ function runPass({registry,roots,stores,mode,setting={},now=Date.now(),budgetMs,
|
|
|
148
148
|
for (const store of ordered) {
|
|
149
149
|
const root=roots[store.root], storeSetting=typeof setting[store.root]==='string'?setting[store.root]:setting[store.root]?.setting || 'on';
|
|
150
150
|
const location=root?path.join(root,...(store.path?store.path.split('/'):[])):'';
|
|
151
|
-
const row
|
|
151
|
+
const row ={id:store.id,root:root || '',location,bound:formatBound(store),setting:storeSetting,entries:0,bytes:0,selected:[],removed:0,protected:0,failed:[]};
|
|
152
152
|
result.stores.push(row);
|
|
153
153
|
if (!root) { row.skipped='absent'; continue; }
|
|
154
154
|
if (ownedPathLinked(store,roots,fsImpl) || resolveStorePath({store,roots,fsImpl})===null) { row.skipped='link'; continue; }
|
|
@@ -158,7 +158,7 @@ function runPass({registry,roots,stores,mode,setting={},now=Date.now(),budgetMs,
|
|
|
158
158
|
if (mode==='prune' && storeSetting==='off' && explicitWindow.state!=='on') { row.skipped='setting-off'; continue; }
|
|
159
159
|
if (mode==='prune') {
|
|
160
160
|
try { sweepTombstones(fsImpl,location,row); }
|
|
161
|
-
catch(error
|
|
161
|
+
catch(error ) { row.failed.push({name:store.id,code:error.code || 'EIO'}); continue; }
|
|
162
162
|
}
|
|
163
163
|
ready.push({store,row,location,explicitWindow,storeSetting});
|
|
164
164
|
}
|
|
@@ -166,7 +166,7 @@ function runPass({registry,roots,stores,mode,setting={},now=Date.now(),budgetMs,
|
|
|
166
166
|
if (shouldStop()) { result.partial=true; break; }
|
|
167
167
|
let entries;
|
|
168
168
|
try { entries=inventory(store,location,fsImpl,shouldStop); }
|
|
169
|
-
catch(error
|
|
169
|
+
catch(error ) { row.failed.push({name:store.id,code:error.code || 'EIO'}); continue; }
|
|
170
170
|
if (entries===null) { result.partial=true; break; }
|
|
171
171
|
row.entries=entries.length;
|
|
172
172
|
for (const entry of entries) {
|
|
@@ -184,17 +184,17 @@ function runPass({registry,roots,stores,mode,setting={},now=Date.now(),budgetMs,
|
|
|
184
184
|
if (mode==='report') {
|
|
185
185
|
let snapshot;
|
|
186
186
|
try { snapshot=readEvents(project,{fsImpl,lockFallback:false,shouldStop}); }
|
|
187
|
-
catch(error
|
|
187
|
+
catch(error ) { row.failed.push({name:store.id,code:error.code || 'EIO'}); continue; }
|
|
188
188
|
if (snapshot.incomplete) { result.partial=true; break; }
|
|
189
189
|
if (snapshot.unreadable.length) { row.failed.push({name:store.id,code:'EIO'}); continue; }
|
|
190
190
|
const selection=selectExpired(snapshot.events,{now:new Date(now),windowMs:window.ms,shouldStop});
|
|
191
191
|
if (selection.incomplete) { result.partial=true; break; }
|
|
192
|
-
row.selected=selection.files.map((name
|
|
192
|
+
row.selected=selection.files.map((name )=>({name,reason:'settled',bytes:stat(fsImpl,path.join(location,name))?.size || 0}));
|
|
193
193
|
} else {
|
|
194
194
|
let output;
|
|
195
195
|
try { output=retainLocked(project,{fsImpl,now:new Date(now),window,maxFiles:Math.max(0,cap-removals),shouldStop}); }
|
|
196
|
-
catch(error
|
|
197
|
-
row.selected=output.selected.map(({name,bytes}
|
|
196
|
+
catch(error ) { row.failed.push({name:store.id,code:error.code || 'EIO'}); continue; }
|
|
197
|
+
row.selected=output.selected.map(({name,bytes} )=>({name,reason:'settled',bytes}));
|
|
198
198
|
row.removed=output.removed; removals+=row.removed;
|
|
199
199
|
if (output.warning.length) row.failed.push({name:store.id,code:output.code || 'EIO'});
|
|
200
200
|
if (output.partial) { result.partial=true; break; }
|
|
@@ -205,7 +205,7 @@ function runPass({registry,roots,stores,mode,setting={},now=Date.now(),budgetMs,
|
|
|
205
205
|
}
|
|
206
206
|
let candidates;
|
|
207
207
|
try { candidates=selectCandidates(store,entries,{registry,roots,fsImpl,storeDir:location,now,settings:registry.settings,shouldStop,cache:workCache}); }
|
|
208
|
-
catch(error
|
|
208
|
+
catch(error ) {
|
|
209
209
|
if (error.partial) { result.partial=true; break; }
|
|
210
210
|
row.failed.push({name:store.id,code:error.code || 'EIO'}); continue;
|
|
211
211
|
}
|
|
@@ -219,8 +219,8 @@ function runPass({registry,roots,stores,mode,setting={},now=Date.now(),budgetMs,
|
|
|
219
219
|
}
|
|
220
220
|
protectedSet=protection.names;
|
|
221
221
|
}
|
|
222
|
-
row.protected=candidates.filter((c
|
|
223
|
-
candidates=candidates.filter((c
|
|
222
|
+
row.protected=candidates.filter((c )=>protectedSet.has(c.name)).length;
|
|
223
|
+
candidates=candidates.filter((c )=>!protectedSet.has(c.name));
|
|
224
224
|
}
|
|
225
225
|
const orderedCandidates=store.entry==='record' ? removalOrder(candidates)
|
|
226
226
|
: [...candidates].sort((a,b)=>a.mtimeMs-b.mtimeMs || a.name.localeCompare(b.name));
|
|
@@ -238,7 +238,7 @@ function runPass({registry,roots,stores,mode,setting={},now=Date.now(),budgetMs,
|
|
|
238
238
|
if (store.entry==='record' && candidate.name.includes('/')) cleanEmptyNamespace(fsImpl,location,candidate.name.split('/')[0]);
|
|
239
239
|
if (store.rule==='report-orphan' && candidate.name.includes('/')) cleanEmptyNamespace(fsImpl,location,candidate.name.split('/')[0],true);
|
|
240
240
|
}
|
|
241
|
-
} catch(error
|
|
241
|
+
} catch(error ) { row.failed.push({name:candidate.name,code:error.code || 'EIO'}); }
|
|
242
242
|
}
|
|
243
243
|
if (store.rule==='report-orphan' && !result.partial) projectKeyMarker(fsImpl,location,roots);
|
|
244
244
|
if (result.partial) break;
|
|
@@ -7,7 +7,7 @@ const path = require('node:path');
|
|
|
7
7
|
|
|
8
8
|
const ROOT_BASES = new Set(['project', 'home', 'xdg-config']);
|
|
9
9
|
const ENTRIES = new Set(['file', 'dir', 'record']);
|
|
10
|
-
const RULES
|
|
10
|
+
const RULES = new Map([
|
|
11
11
|
['count-and-age', ['keepNewest', 'maxAgeDays']],
|
|
12
12
|
['work', ['finishedDays']],
|
|
13
13
|
['date-partition', ['maxAgeDays']],
|
|
@@ -21,13 +21,13 @@ const RULES: Map<string, string[]> = new Map([
|
|
|
21
21
|
const MOMENTS = new Set(['verb', 'session', 'verify', 'install', 'lifecycle-verb', 'uninstall']);
|
|
22
22
|
const STORE_ID = /^[a-z][a-z0-9-]*$/;
|
|
23
23
|
|
|
24
|
-
function isObject(value
|
|
25
|
-
function positiveInteger(value
|
|
26
|
-
function error(errors
|
|
24
|
+
function isObject(value ) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
|
25
|
+
function positiveInteger(value ) { return Number.isInteger(value) && value > 0; }
|
|
26
|
+
function error(errors , where , message ) { errors.push(`${where}: ${message}`); }
|
|
27
27
|
|
|
28
28
|
/** Validates the closed IC-001 storage declaration document without touching the filesystem. */
|
|
29
|
-
function validateStorageRegistry(registry
|
|
30
|
-
const errors
|
|
29
|
+
function validateStorageRegistry(registry ) {
|
|
30
|
+
const errors = [];
|
|
31
31
|
if (!isObject(registry)) return { ok: false, errors: ['registry: must be an object'] };
|
|
32
32
|
if (registry.version !== 1) error(errors, 'version', 'must be 1');
|
|
33
33
|
if (typeof registry.description !== 'string' || !registry.description.trim()) error(errors, 'description', 'must be a non-empty string');
|
|
@@ -39,28 +39,28 @@ function validateStorageRegistry(registry: any): { ok: boolean; errors: string[]
|
|
|
39
39
|
if (!positiveInteger(settings[field])) error(errors, `settings.${field}`, 'must be a positive integer');
|
|
40
40
|
}
|
|
41
41
|
if (!isObject(settings.ignoreFile) || settings.ignoreFile.name !== '.gitignore'
|
|
42
|
-
|| !Array.isArray(settings.ignoreFile.lines) || settings.ignoreFile.lines.some((line
|
|
42
|
+
|| !Array.isArray(settings.ignoreFile.lines) || settings.ignoreFile.lines.some((line ) => typeof line !== 'string')) {
|
|
43
43
|
error(errors, 'settings.ignoreFile', 'must name .gitignore and contain string lines');
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
const roots = registry.roots;
|
|
47
47
|
if (!isObject(roots) || Object.keys(roots).length === 0) error(errors, 'roots', 'must be a non-empty object');
|
|
48
|
-
else for (const [id, root] of Object.entries(roots)
|
|
48
|
+
else for (const [id, root] of Object.entries(roots) ) {
|
|
49
49
|
if (!isObject(root) || !ROOT_BASES.has(root.base) || !Array.isArray(root.segments)
|
|
50
|
-
|| root.segments.some((segment
|
|
50
|
+
|| root.segments.some((segment ) => typeof segment !== 'string' || !segment || segment === '.' || segment === '..' || segment.includes('/') || segment.includes('\\'))) {
|
|
51
51
|
error(errors, `roots.${id}`, 'must have a known base and safe path segments');
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
if (!Array.isArray(registry.stores)) error(errors, 'stores', 'must be an array');
|
|
55
55
|
else {
|
|
56
56
|
const ids = new Set();
|
|
57
|
-
registry.stores.forEach((store
|
|
57
|
+
registry.stores.forEach((store , index ) => {
|
|
58
58
|
const where = `stores[${index}]`;
|
|
59
59
|
if (!isObject(store)) { error(errors, where, 'must be an object'); return; }
|
|
60
60
|
if (typeof store.id !== 'string' || !STORE_ID.test(store.id)) error(errors, `${where}.id`, 'must match ^[a-z][a-z0-9-]*$');
|
|
61
61
|
else if (ids.has(store.id)) error(errors, `${where}.id`, `duplicate id '${store.id}'`); else ids.add(store.id);
|
|
62
62
|
if (typeof store.root !== 'string' || !roots?.[store.root]) error(errors, `${where}.root`, 'must name a declared root');
|
|
63
|
-
if (typeof store.path !== 'string' || store.path.startsWith('/') || store.path.split('/').some((part
|
|
63
|
+
if (typeof store.path !== 'string' || store.path.startsWith('/') || store.path.split('/').some((part ) => part === '..' || !part && store.path !== '')) error(errors, `${where}.path`, 'must be relative, slash-separated, and contain no ..');
|
|
64
64
|
if (!ENTRIES.has(store.entry)) error(errors, `${where}.entry`, 'must be file, dir, or record');
|
|
65
65
|
if (typeof store.match !== 'string' || !store.match.startsWith('^') || !store.match.endsWith('$')) error(errors, `${where}.match`, 'must be an anchored regular expression');
|
|
66
66
|
else { try { new RegExp(store.match); } catch { error(errors, `${where}.match`, 'must compile'); } }
|
|
@@ -72,9 +72,9 @@ function validateStorageRegistry(registry: any): { ok: boolean; errors: string[]
|
|
|
72
72
|
if (store.rule === 'existing' && store.bound.value !== undefined && !positiveInteger(store.bound.value)) error(errors, `${where}.bound.value`, 'must be a positive integer when present');
|
|
73
73
|
}
|
|
74
74
|
if (store.protect !== null && store.protect !== 'open-work') error(errors, `${where}.protect`, 'must be open-work or null');
|
|
75
|
-
if (!Array.isArray(store.moments) || store.moments.some((moment
|
|
75
|
+
if (!Array.isArray(store.moments) || store.moments.some((moment ) => !MOMENTS.has(moment)) || new Set(store.moments).size !== store.moments.length) error(errors, `${where}.moments`, 'must be a unique subset of known moments');
|
|
76
76
|
if (store.rule === 'existing' && store.moments?.length) error(errors, `${where}.moments`, 'must be empty for existing stores');
|
|
77
|
-
if (!Array.isArray(store.writers) || store.writers.some((writer
|
|
77
|
+
if (!Array.isArray(store.writers) || store.writers.some((writer ) => typeof writer !== 'string' || !writer)) error(errors, `${where}.writers`, 'must be an array of repository-relative paths');
|
|
78
78
|
else if (!store.writers.length && store.rule !== 'control' && !store.id.startsWith('user-settings-')) error(errors, `${where}.writers`, 'may be empty only for control or user-settings stores');
|
|
79
79
|
if (store.rule === 'existing') {
|
|
80
80
|
if (!isObject(store.source) || typeof store.source.file !== 'string' || !store.source.file
|
|
@@ -87,17 +87,17 @@ function validateStorageRegistry(registry: any): { ok: boolean; errors: string[]
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
if (!Array.isArray(registry.externalWriters)) error(errors, 'externalWriters', 'must be an array');
|
|
90
|
-
else registry.externalWriters.forEach((writer
|
|
90
|
+
else registry.externalWriters.forEach((writer , index ) => {
|
|
91
91
|
if (!isObject(writer) || typeof writer.file !== 'string' || !writer.file || typeof writer.reason !== 'string' || !writer.reason.trim()) error(errors, `externalWriters[${index}]`, 'requires a file and non-empty reason');
|
|
92
92
|
});
|
|
93
93
|
return { ok: errors.length === 0, errors };
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/** Loads and fails closed on an IC-001 storage registry. */
|
|
97
|
-
function loadStorageRegistry({ repoRoot, file, fsImpl = fs }
|
|
97
|
+
function loadStorageRegistry({ repoRoot, file, fsImpl = fs } = {}) {
|
|
98
98
|
const target = file || path.join(path.resolve(repoRoot || process.cwd()), 'core', 'registry', 'storage.json');
|
|
99
|
-
let registry
|
|
100
|
-
try { registry = JSON.parse(fsImpl.readFileSync(target, 'utf8')); } catch (cause
|
|
99
|
+
let registry ;
|
|
100
|
+
try { registry = JSON.parse(fsImpl.readFileSync(target, 'utf8')); } catch (cause ) { throw new Error(`Could not load storage registry '${target}': ${cause.message}`); }
|
|
101
101
|
const validation = validateStorageRegistry(registry);
|
|
102
102
|
if (!validation.ok) throw new Error(`Invalid storage registry:\n- ${validation.errors.join('\n- ')}`);
|
|
103
103
|
return registry;
|
|
@@ -105,13 +105,13 @@ function loadStorageRegistry({ repoRoot, file, fsImpl = fs }: any = {}): any {
|
|
|
105
105
|
|
|
106
106
|
/** Renders IC-001's canonical human-readable bound for a store. */
|
|
107
107
|
/** Renders an existing-store source pattern for G26's literal source check. */
|
|
108
|
-
function renderSourcePattern(store
|
|
108
|
+
function renderSourcePattern(store ) {
|
|
109
109
|
const pattern = store?.source?.pattern;
|
|
110
110
|
if (typeof pattern !== 'string') throw new Error('Existing store has no source pattern');
|
|
111
111
|
return pattern.includes('{value}') ? pattern.replaceAll('{value}', String(store.bound.value)) : pattern;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
function formatBound(store
|
|
114
|
+
function formatBound(store ) {
|
|
115
115
|
const bound = store.bound;
|
|
116
116
|
switch (store.rule) {
|
|
117
117
|
case 'count-and-age': return `keep newest ${bound.keepNewest}; prune entries older than ${bound.maxAgeDays} days`;
|
|
@@ -6,22 +6,22 @@ const fs = require('node:fs');
|
|
|
6
6
|
const os = require('node:os');
|
|
7
7
|
const path = require('node:path');
|
|
8
8
|
|
|
9
|
-
function absolute(value
|
|
9
|
+
function absolute(value ) { return typeof value === 'string' && path.isAbsolute(value) ? value : null; }
|
|
10
10
|
|
|
11
11
|
/** Resolves IC-001's five physical roots; -g/global deliberately omits project roots. */
|
|
12
|
-
function resolveRoots({ registry, projectRoot, global = false, env = process.env }
|
|
12
|
+
function resolveRoots({ registry, projectRoot, global = false, env = process.env } = {}) {
|
|
13
13
|
const home = absolute(env.HOME) || os.homedir();
|
|
14
14
|
const xdgBase = env.XDG_CONFIG_HOME === undefined || env.XDG_CONFIG_HOME === ''
|
|
15
15
|
? path.join(home, '.config') : absolute(env.XDG_CONFIG_HOME);
|
|
16
16
|
const project = global ? null : absolute(projectRoot);
|
|
17
|
-
const bases
|
|
18
|
-
return Object.fromEntries(Object.entries(registry.roots).map(([id, root]
|
|
17
|
+
const bases = { project, home, 'xdg-config': xdgBase };
|
|
18
|
+
return Object.fromEntries(Object.entries(registry.roots).map(([id, root] ) => {
|
|
19
19
|
const base = bases[root.base];
|
|
20
20
|
return [id, base ? path.join(base, ...root.segments) : null];
|
|
21
21
|
}));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function readSetting(file
|
|
24
|
+
function readSetting(file , fsImpl ) {
|
|
25
25
|
try {
|
|
26
26
|
const first = fsImpl.readFileSync(file, 'utf8').split(/\r?\n/, 1)[0].trim().toLowerCase();
|
|
27
27
|
return first === 'on' || first === 'off' ? first : 'off';
|
|
@@ -29,7 +29,7 @@ function readSetting(file: string, fsImpl: any): string {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** Reads IC-014 and reports exactly which present file made the decision, if any. */
|
|
32
|
-
function readPruneSetting({ registry, roots, rootId, fsImpl = fs }
|
|
32
|
+
function readPruneSetting({ registry, roots, rootId, fsImpl = fs } = {}) {
|
|
33
33
|
const projectFile = roots['project-doflow'] ? path.join(roots['project-doflow'], registry.settings.optOutFile) : null;
|
|
34
34
|
const userFile = roots['config-home'] ? path.join(roots['config-home'], registry.settings.optOutFile) : null;
|
|
35
35
|
const usesProject = rootId === 'project-doflow' || rootId === 'project-state';
|
|
@@ -38,7 +38,7 @@ function readPruneSetting({ registry, roots, rootId, fsImpl = fs }: any = {}): {
|
|
|
38
38
|
return { setting: 'on', decidedBy: null };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function linked(file
|
|
41
|
+
function linked(file , fsImpl ) {
|
|
42
42
|
try { return fsImpl.lstatSync(file).isSymbolicLink(); } catch { return false; }
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -46,7 +46,7 @@ function linked(file: string, fsImpl: any): boolean {
|
|
|
46
46
|
* Returns the physical directory for a declared store, or null when a DoFlow-owned component is a
|
|
47
47
|
* link. The engine treats null as a refuse-to-touch result, never as a path to repair.
|
|
48
48
|
*/
|
|
49
|
-
function resolveStorePath({ store, roots, fsImpl = fs }
|
|
49
|
+
function resolveStorePath({ store, roots, fsImpl = fs } = {}) {
|
|
50
50
|
const root = roots[store.root];
|
|
51
51
|
if (!root) return null;
|
|
52
52
|
if (linked(root, fsImpl)) return null;
|