@cassiomc1/forgeloop 0.1.9 → 0.1.10
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/README.md +45 -6
- package/THREAT_MODEL.md +5 -0
- package/conformance/README.md +35 -0
- package/package.json +1 -1
- package/src/commands/update.js +30 -19
- package/src/core/checks.js +19 -8
- package/src/core/completion-relationships.js +2 -2
package/README.md
CHANGED
|
@@ -61,11 +61,17 @@ flowchart TB
|
|
|
61
61
|
root --> entry["native adapters<br/>+ .forgeloop/kit/"]
|
|
62
62
|
entry --> migration{"target<br/>layout"}
|
|
63
63
|
migration -->|legacy v1| plan["validate paths<br/>+ build plan"]
|
|
64
|
-
plan --> hidden["write
|
|
65
|
-
hidden -->
|
|
64
|
+
plan --> hidden["write hidden<br/>kit"]
|
|
65
|
+
hidden --> verified["verify hidden<br/>bytes"]
|
|
66
|
+
verified --> authority["atomic manifest<br/>authority switch"]
|
|
66
67
|
authority --> cleanup["hash-checked<br/>legacy cleanup"]
|
|
67
68
|
cleanup --> recovered["healthy or<br/>recoverable"]
|
|
69
|
+
hidden -. interruption .-> incomplete["doctor: E_MIGRATION_INCOMPLETE"]
|
|
70
|
+
verified -. interruption .-> incomplete
|
|
71
|
+
authority -. interruption .-> incomplete
|
|
72
|
+
cleanup -. interruption .-> incomplete
|
|
68
73
|
migration -->|layout v2| retry["update + retry<br/>owned cleanup"]
|
|
74
|
+
incomplete --> retry
|
|
69
75
|
entry --> discovery["discovery +<br/>project profile"]
|
|
70
76
|
discovery --> contract["current<br/>contract"]
|
|
71
77
|
contract --> routing["deterministic<br/>route"]
|
|
@@ -115,7 +121,7 @@ flowchart TB
|
|
|
115
121
|
|
|
116
122
|
class root root;
|
|
117
123
|
class entry,discovery,delegation entry;
|
|
118
|
-
class migration,plan,hidden,authority,cleanup,recovered,retry migration;
|
|
124
|
+
class migration,plan,hidden,verified,authority,cleanup,recovered,retry,incomplete migration;
|
|
119
125
|
class routing,preflight,ready routing;
|
|
120
126
|
class state,events,lifecycle state;
|
|
121
127
|
class checks,receipt,audit evidence;
|
|
@@ -127,9 +133,11 @@ flowchart TB
|
|
|
127
133
|
```
|
|
128
134
|
|
|
129
135
|
Equivalent reading for text-only environments: adapters load the canonical kit;
|
|
130
|
-
an older target follows validate paths → write
|
|
131
|
-
manifest authority → hash-checked cleanup
|
|
132
|
-
|
|
136
|
+
an older target follows validate paths → write hidden files → verify their bytes
|
|
137
|
+
→ switch manifest authority atomically → hash-checked cleanup. An interruption
|
|
138
|
+
after hidden writes, after verification, after the authority switch, or during
|
|
139
|
+
cleanup is diagnosed by `doctor` as `E_MIGRATION_INCOMPLETE` and retried by
|
|
140
|
+
`update`; modified or unmanaged residual files remain preserved;
|
|
133
141
|
discovery creates the contract and deterministic route; contract, route, and
|
|
134
142
|
required gates must produce `PREFLIGHT_READY` before the resumable state and
|
|
135
143
|
append-only event ledger authorize the lifecycle. Verification produces
|
|
@@ -150,6 +158,37 @@ Antigravity, OpenCode, Hermes, Pi, Command Code, and Freebuff use the shared
|
|
|
150
158
|
documents; see [`AGENT_COMPATIBILITY.md`](./AGENT_COMPATIBILITY.md) for the
|
|
151
159
|
official sources and precedence notes.
|
|
152
160
|
|
|
161
|
+
### Migration recovery and release freeze
|
|
162
|
+
|
|
163
|
+
Legacy layout migration keeps `.forgeloop/kit/` as the canonical layout and
|
|
164
|
+
does not make `layoutVersion: 2` authoritative until every planned hidden file
|
|
165
|
+
has been written and byte-verified. Cleanup runs only after the manifest switch
|
|
166
|
+
and only for legacy files whose recorded ownership hash still matches when
|
|
167
|
+
ForgeLoop revalidates it immediately before deletion. A modified, unmanaged,
|
|
168
|
+
or `preserve=true` file is retained for manual review.
|
|
169
|
+
|
|
170
|
+
The interruption vocabulary is test-only and is not a runtime state machine:
|
|
171
|
+
|
|
172
|
+
```text
|
|
173
|
+
VALIDATED → HIDDEN_WRITTEN → HIDDEN_VERIFIED → MANIFEST_SWITCHED
|
|
174
|
+
→ LEGACY_CLEANED → COMPLETE
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The regression suite injects failures at these boundaries and verifies that
|
|
178
|
+
`doctor` explains the incomplete migration before a later `update` recovers
|
|
179
|
+
owned cleanup. The frozen published installation under
|
|
180
|
+
[`tests/fixtures/legacy-0.1.6/`](./tests/fixtures/legacy-0.1.6/) is derived
|
|
181
|
+
from the real npm tarball, includes provenance and digests, and is copied into
|
|
182
|
+
tests locally; CI does not download npm packages.
|
|
183
|
+
|
|
184
|
+
The current published baseline for a reproducible blind run is
|
|
185
|
+
`@cassiomc1/forgeloop@0.1.9`. The supplied hardening brief's `0.1.8` reference
|
|
186
|
+
is historical; never move `v0.1.8` or `v0.1.9`, and use the read-only release
|
|
187
|
+
identity verifier before a live run. The repository candidate is `0.1.10`, but
|
|
188
|
+
it is not published: its completion-validation and cleanup TOCTOU fixes are not
|
|
189
|
+
part of the frozen `0.1.9` tarball. Publish, tag, and repeat identity
|
|
190
|
+
verification before a blind run that needs those executable changes.
|
|
191
|
+
|
|
153
192
|
### Use with npm
|
|
154
193
|
|
|
155
194
|
The npm CLI targets Node.js 20 or newer and installs the kit into an existing
|
package/THREAT_MODEL.md
CHANGED
|
@@ -13,6 +13,7 @@ remaining trust boundaries and their executable evidence.
|
|
|
13
13
|
| Path traversal | Writes or reads outside the selected target | Target path and every managed relative path | `ensureWithin`, safe-path checks, realpath containment, Windows-drive rejection | A separately privileged process can change the filesystem after validation | `tests/core.test.js`, `tests/portability.test.js`, `tests/fixtures/protocol/invalid/path-traversal.json` |
|
|
14
14
|
| Symlink escape | Redirects a managed path to another directory | Existing target parents and artifact paths | Reject symlinked targets, parents, and destinations before access | The check is not a filesystem lock | `tests/core.test.js`, `tests/portability.test.js`, `tests/fixtures/protocol/invalid/symlink-target.json` |
|
|
15
15
|
| Malicious manifest | Causes unsafe update or false ownership claims | `.forgeloop/manifest.json` | Schema, managed/legacy hash, safe-path, and preserve-flag validation; incomplete migration remains visible | A trusted operator can still deliberately edit the manifest | `tests/cli.test.js`, `tests/core.test.js`, `tests/hidden-layout.test.js` |
|
|
16
|
+
| Interrupted or concurrent migration cleanup | Leaves a new hidden kit, an old manifest, residual legacy files, or mutates a legacy file after planning | Hidden kit, manifest authority, and legacy root cleanup | Validate all paths, write and verify the hidden plan, atomically switch authority, then revalidate safe paths and recorded ownership hashes immediately before unlink; mismatches are preserved and `doctor` reports `E_MIGRATION_INCOMPLETE` | Hash-before-unlink narrows but cannot eliminate the final OS-level race without filesystem locking; a separately privileged process remains outside the trust boundary | `tests/hidden-layout.test.js`, `README.md` |
|
|
16
17
|
| Malicious work-state | Resumes stale, secret-bearing, or invalid work | `.forgeloop/work-state.json` | Schema/semantic checks, version checks, transition checks, contract/HEAD/artifact freshness, secret scan, size/depth bounds | Freshness cannot prove that an external process did not alter a file immediately afterward | `tests/work-state.test.js`, `tests/checkpoint-freshness.test.js`, `tests/security-limits.test.js` |
|
|
17
18
|
| Forged preparation | Makes an agent's prose claim look like a completed preflight | Contract, route, gate, and preflight artifacts | Canonical SHA-256 fingerprints, guide-declared gate requirements, stale-artifact checks, and `E_*` failures | The CLI cannot stop a separate process from writing project files before preflight | `tests/protocol-artifacts.test.js`, `tests/preflight.test.js` |
|
|
18
19
|
| Chronology rewrite | Hides execution before route, gates, or verification | `.forgeloop/events.ndjson` | Append-only local ledger, sequence numbers, hash chaining, and chronology validation without prompts or hidden reasoning | A privileged process can still replace the ledger after validation | `tests/lifecycle.test.js` |
|
|
@@ -23,6 +24,7 @@ remaining trust boundaries and their executable evidence.
|
|
|
23
24
|
| Artifact content exposure | Sensitive material enters portable artifacts or diagnostics | State, receipt, delegation, evidence, and repository text | Nested key/value detection, shaped marker patterns, scanner coverage, no content echo in errors | Content scanners cannot prove that an unknown encoding is harmless | `tests/security-limits.test.js`, `tests/test_scan_secrets.py`, `scripts/scan_secrets.py` |
|
|
24
25
|
| Stale checkpoint replay | Repeats work against a changed contract or material file | Resume boundary between a saved state and current target | Contract fingerprint, repository fingerprint, required-artifact hashes, protocol version, and conservative revalidation | A contract author must provide the current contract file for comparison | `tests/checkpoint-freshness.test.js`, `tests/cli.test.js` |
|
|
25
26
|
| Unsafe publication claim | Local output is mistaken for push, PR, merge, or deployment | Receipt publication fields and evidence | Publication starts false; each true claim requires matching evidence; no CLI publication action | Remote provider state is outside local verification | `tests/receipt-semantics.test.js`, `README.md` |
|
|
27
|
+
| Release identity drift | A blind run uses a package whose code, tag, commit, or tarball digest differs from the stated release | npm metadata, GitHub tag, and release commit | Read-only verifier requires package version, `gitHead`, tag, tarball URL, SHA-1, and SHA-512 to agree before the prompt starts | Registry and remote state require a fresh check for every release; a documentation commit is not a new npm package | `tests/release-identity.test.js`, `scripts/verify_release_identity.mjs`, `conformance/README.md` |
|
|
26
28
|
| Schema confusion | A malformed or altered schema is treated as available | Shipped and target-local `schemas/` directory | Health checks parse every schema and classify valid, missing, invalid, or unsupported-version | Schema health does not replace review of semantic relationships | `tests/schema-health.test.js`, `src/core/schema-validation.js` |
|
|
27
29
|
| Protocol-version confusion | Old/future artifacts are silently reinterpreted | Cross-artifact protocol boundary | Explicit v1 checks and conformance errors for unsupported or mixed versions | A future protocol needs a deliberate migration implementation | `tests/conformance.test.js`, `tests/fixtures/protocol/invalid/` |
|
|
28
30
|
| Dependency-cycle denial of service | Coordination cannot make progress or spends unbounded work | Delegation dependency graph | Deterministic DFS cycle detection, unknown-reference rejection, and bounded JSON | A caller can still submit many valid tasks; host-level quotas remain external | `tests/delegation.test.js`, `tests/delegation-set.test.js` |
|
|
@@ -32,6 +34,9 @@ remaining trust boundaries and their executable evidence.
|
|
|
32
34
|
|
|
33
35
|
- Safe paths are checked before reading or writing; no protocol field is a
|
|
34
36
|
shell command.
|
|
37
|
+
- A migration never treats `layoutVersion: 2` as authoritative before the
|
|
38
|
+
hidden kit is complete and verified; cleanup follows the authority switch and
|
|
39
|
+
is limited by recorded ownership hashes.
|
|
35
40
|
- A `NOT_VERIFIED` or `BLOCKED` evidence record is never upgraded to observed
|
|
36
41
|
success by a formatter.
|
|
37
42
|
- Local checks do not imply remote publication, merge, deployment, or provider
|
package/conformance/README.md
CHANGED
|
@@ -8,6 +8,26 @@ The current published baseline for new runs is
|
|
|
8
8
|
`@cassiomc1/forgeloop@0.1.9`. Pin that version when preparing a reproducible
|
|
9
9
|
blind run; historical reports retain the exact package version they used.
|
|
10
10
|
|
|
11
|
+
The frozen baseline was verified on 2026-08-13 with this identity:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
package: @cassiomc1/forgeloop@0.1.9
|
|
15
|
+
npm gitHead: 0e48240894671ecda2458b15671020b0a7255e70
|
|
16
|
+
release commit: 0e48240894671ecda2458b15671020b0a7255e70
|
|
17
|
+
GitHub tag: v0.1.9 -> 0e48240894671ecda2458b15671020b0a7255e70
|
|
18
|
+
tarball URL: https://registry.npmjs.org/@cassiomc1/forgeloop/-/forgeloop-0.1.9.tgz
|
|
19
|
+
tarball SHA-1: fef7587ebd878e845e9746b4d049a6fe6ef333c5
|
|
20
|
+
npm SHA-512 integrity: sha512-sCvPEDOYT1svNnA8+zw57dJ97N6sNCUH8eDbM6f3cS6eXLCIFFXRPkCAASA+ZPu5mAKbcNaU1TaFKjojbL3DEA==
|
|
21
|
+
release identity: RELEASE_IDENTITY_VALID
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The repository may contain documentation or executable commits after this
|
|
25
|
+
frozen package. Those commits do not change the package used by the blind run;
|
|
26
|
+
the repository's `0.1.10` candidate includes completion-validation and cleanup
|
|
27
|
+
TOCTOU fixes that are not in `0.1.9`. If a blind run needs those executable
|
|
28
|
+
changes, publish and tag the candidate, then repeat the complete identity check
|
|
29
|
+
before starting the run.
|
|
30
|
+
|
|
11
31
|
Run a scenario in a disposable target using the Standard profile first:
|
|
12
32
|
|
|
13
33
|
```bash
|
|
@@ -78,6 +98,21 @@ field, record `RELEASE_IDENTITY_NOT_VERIFIED` or
|
|
|
78
98
|
The complete-website scenario deliberately fails when implementation starts
|
|
79
99
|
before the contract, route, and required gates exist.
|
|
80
100
|
|
|
101
|
+
## Migration compatibility evidence
|
|
102
|
+
|
|
103
|
+
The real published `0.1.6` fixture at
|
|
104
|
+
[`tests/fixtures/legacy-0.1.6/`](../tests/fixtures/legacy-0.1.6/) is frozen
|
|
105
|
+
with package, tarball, SHA-1, SHA-512, `gitHead`, and extraction-date metadata.
|
|
106
|
+
The migration tests run from those local bytes and cover interruptions after
|
|
107
|
+
hidden writes, after hidden verification, after the manifest authority switch,
|
|
108
|
+
and during legacy cleanup. An interrupted target must be diagnosed as
|
|
109
|
+
`E_MIGRATION_INCOMPLETE`; a later `update` may retry only hash-owned cleanup.
|
|
110
|
+
User-modified, unmanaged, and `preserve=true` files remain untouched even when
|
|
111
|
+
that leaves a root residual. ForgeLoop revalidates the recorded ownership hash
|
|
112
|
+
immediately before deleting each managed legacy file. This narrows the race
|
|
113
|
+
window but does not provide OS-level filesystem locking against a separately
|
|
114
|
+
privileged concurrent process.
|
|
115
|
+
|
|
81
116
|
## Autonomous blind-run isolation
|
|
82
117
|
|
|
83
118
|
External workflows may help with local planning, review, tests, or
|
package/package.json
CHANGED
package/src/commands/update.js
CHANGED
|
@@ -41,8 +41,8 @@ async function removeEmptyLegacyDirectory(target, relativePath, dryRun) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function addLegacyCleanup(cleanupFiles, cleanupDirectories, relativePath) {
|
|
45
|
-
cleanupFiles.
|
|
44
|
+
function addLegacyCleanup(cleanupFiles, cleanupDirectories, relativePath, expectedLegacySha256) {
|
|
45
|
+
cleanupFiles.set(relativePath, expectedLegacySha256);
|
|
46
46
|
addLegacyCleanupDirectory(cleanupDirectories, relativePath);
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -66,14 +66,25 @@ async function notifyStage(hooks, stage, context) {
|
|
|
66
66
|
if (typeof hooks?.afterStage === "function") await hooks.afterStage(stage, context);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
async function cleanupLegacyFiles({ target, dryRun, cleanupFiles, cleanupDirectories, hooks }) {
|
|
70
|
-
for (const relativePath of cleanupFiles) {
|
|
71
|
-
await assertSafePath(target, relativePath);
|
|
69
|
+
async function cleanupLegacyFiles({ target, dryRun, cleanupFiles, cleanupDirectories, hooks, actions, conflicts }) {
|
|
70
|
+
for (const [relativePath, expectedLegacySha256] of cleanupFiles) {
|
|
72
71
|
if (typeof hooks?.beforeCleanup === "function") {
|
|
73
72
|
await hooks.beforeCleanup(relativePath);
|
|
74
73
|
}
|
|
74
|
+
await assertSafePath(target, relativePath);
|
|
75
75
|
const legacyPath = ensureWithin(target, relativePath);
|
|
76
|
-
if (
|
|
76
|
+
if (dryRun || !(await fileExists(legacyPath))) continue;
|
|
77
|
+
const currentLegacySha256 = sha256(await readBytes(legacyPath));
|
|
78
|
+
if (currentLegacySha256 !== expectedLegacySha256) {
|
|
79
|
+
conflicts.push(migrationConflict(
|
|
80
|
+
"E_LEGACY_FILE_MIGRATION_CONFLICT",
|
|
81
|
+
relativePath,
|
|
82
|
+
"Legacy file changed before cleanup; it was preserved.",
|
|
83
|
+
));
|
|
84
|
+
actions.push({ action: "preserve-conflict", path: relativePath, reason: "changed-before-cleanup" });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
await unlink(legacyPath);
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
for (const relativePath of cleanupDirectories) {
|
|
@@ -89,7 +100,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
89
100
|
const actions = [];
|
|
90
101
|
const conflicts = [];
|
|
91
102
|
const writes = [];
|
|
92
|
-
const cleanupFiles = new
|
|
103
|
+
const cleanupFiles = new Map();
|
|
93
104
|
const cleanupDirectories = new Set();
|
|
94
105
|
|
|
95
106
|
// Validate every path before creating the migration plan or touching data.
|
|
@@ -163,7 +174,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
163
174
|
&& legacyRecord
|
|
164
175
|
&& currentHash === legacyHash) {
|
|
165
176
|
actions.push({ action: "skip", path: entry.relativePath, reason: "profile-move-resumed" });
|
|
166
|
-
addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath);
|
|
177
|
+
addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath, legacyHash);
|
|
167
178
|
nextManifest.files[entry.relativePath] = manifestRecord(currentHash, true, legacyHash);
|
|
168
179
|
continue;
|
|
169
180
|
}
|
|
@@ -177,7 +188,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
177
188
|
addAction(actions, dryRun, "preserve-conflict", entry.relativePath, { reason: "hidden-unmanaged" });
|
|
178
189
|
} else if (hiddenMatchesSource && legacyExists && unchangedManagedLegacy) {
|
|
179
190
|
actions.push({ action: "skip", path: entry.relativePath, reason: "hidden-ready" });
|
|
180
|
-
addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath);
|
|
191
|
+
addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath, legacyHash);
|
|
181
192
|
nextManifest.files[entry.relativePath] = manifestRecord(
|
|
182
193
|
sourceHash,
|
|
183
194
|
entry.sourcePath === PROFILE_PATH,
|
|
@@ -224,7 +235,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
224
235
|
if (entry.sourcePath === PROFILE_PATH && legacyRecord) {
|
|
225
236
|
addAction(actions, dryRun, "move-profile", entry.legacyRelativePath, { to: entry.relativePath });
|
|
226
237
|
writes.push({ destination, bytes: legacyBytes });
|
|
227
|
-
addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath);
|
|
238
|
+
addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath, legacyHash);
|
|
228
239
|
nextManifest.files[entry.relativePath] = manifestRecord(legacyHash, true, legacyHash);
|
|
229
240
|
continue;
|
|
230
241
|
}
|
|
@@ -232,7 +243,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
232
243
|
if (legacyRecord?.preserve && unchangedManaged) {
|
|
233
244
|
addAction(actions, dryRun, "move-preserved", entry.legacyRelativePath, { to: entry.relativePath });
|
|
234
245
|
writes.push({ destination, bytes: legacyBytes });
|
|
235
|
-
addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath);
|
|
246
|
+
addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath, legacyHash);
|
|
236
247
|
nextManifest.files[entry.relativePath] = manifestRecord(legacyHash, true, legacyHash);
|
|
237
248
|
continue;
|
|
238
249
|
}
|
|
@@ -240,7 +251,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
240
251
|
if (unchangedManaged) {
|
|
241
252
|
addAction(actions, dryRun, "migrate", entry.legacyRelativePath, { to: entry.relativePath });
|
|
242
253
|
writes.push({ destination, bytes: entry.bytes });
|
|
243
|
-
addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath);
|
|
254
|
+
addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath, legacyHash);
|
|
244
255
|
nextManifest.files[entry.relativePath] = manifestRecord(sourceHash, false, legacyHash);
|
|
245
256
|
continue;
|
|
246
257
|
}
|
|
@@ -278,10 +289,10 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
278
289
|
// Atomic manifest replacement is the authority switch. Cleanup follows it and
|
|
279
290
|
// remains recoverable because managed legacy hashes are retained in the record.
|
|
280
291
|
await writeManifest(target, nextManifest, { dryRun });
|
|
281
|
-
await notifyStage(hooks, "MANIFEST_SWITCHED", { cleanupFiles: [...cleanupFiles] });
|
|
282
|
-
await cleanupLegacyFiles({ target, dryRun, cleanupFiles, cleanupDirectories, hooks });
|
|
283
|
-
await notifyStage(hooks, "LEGACY_CLEANED", { cleanupFiles: [...cleanupFiles] });
|
|
284
|
-
await notifyStage(hooks, "COMPLETE", { cleanupFiles: [...cleanupFiles] });
|
|
292
|
+
await notifyStage(hooks, "MANIFEST_SWITCHED", { cleanupFiles: [...cleanupFiles.keys()] });
|
|
293
|
+
await cleanupLegacyFiles({ target, dryRun, cleanupFiles, cleanupDirectories, hooks, actions, conflicts });
|
|
294
|
+
await notifyStage(hooks, "LEGACY_CLEANED", { cleanupFiles: [...cleanupFiles.keys()] });
|
|
295
|
+
await notifyStage(hooks, "COMPLETE", { cleanupFiles: [...cleanupFiles.keys()] });
|
|
285
296
|
return { actions, conflicts, manifest: nextManifest };
|
|
286
297
|
}
|
|
287
298
|
|
|
@@ -300,7 +311,7 @@ export async function runUpdate({ target, dryRun, packageRoot, packageVersion, h
|
|
|
300
311
|
const actions = [];
|
|
301
312
|
const conflicts = [];
|
|
302
313
|
const plans = [];
|
|
303
|
-
const cleanupFiles = new
|
|
314
|
+
const cleanupFiles = new Map();
|
|
304
315
|
const cleanupDirectories = new Set();
|
|
305
316
|
const pruneActions = [];
|
|
306
317
|
const shippedPaths = new Set(entries.map((entry) => entry.relativePath));
|
|
@@ -333,7 +344,7 @@ export async function runUpdate({ target, dryRun, packageRoot, packageVersion, h
|
|
|
333
344
|
if (hasLegacyAlternative && await fileExists(legacyDestination)) {
|
|
334
345
|
const legacyHash = sha256(await readBytes(legacyDestination));
|
|
335
346
|
if (record?.legacySha256 && legacyHash === record.legacySha256) {
|
|
336
|
-
addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath);
|
|
347
|
+
addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath, record.legacySha256);
|
|
337
348
|
} else {
|
|
338
349
|
conflicts.push({
|
|
339
350
|
code: "E_LEGACY_FILE_MIGRATION_CONFLICT",
|
|
@@ -412,6 +423,6 @@ export async function runUpdate({ target, dryRun, packageRoot, packageVersion, h
|
|
|
412
423
|
nextManifest.packageName = PACKAGE_NAME;
|
|
413
424
|
nextManifest.packageVersion = packageVersion;
|
|
414
425
|
await writeManifest(target, nextManifest, { dryRun });
|
|
415
|
-
await cleanupLegacyFiles({ target, dryRun, cleanupFiles, cleanupDirectories, hooks });
|
|
426
|
+
await cleanupLegacyFiles({ target, dryRun, cleanupFiles, cleanupDirectories, hooks, actions, conflicts });
|
|
416
427
|
return { actions, conflicts, manifest: nextManifest };
|
|
417
428
|
}
|
package/src/core/checks.js
CHANGED
|
@@ -92,24 +92,35 @@ export function assertCheckList(value, label = "checks") {
|
|
|
92
92
|
return value;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
function requiredChecksSatisfiedBy(checks, requiredValues, selector, { allowInferred = false } = {}) {
|
|
96
96
|
assertCheckList(checks);
|
|
97
|
-
if (!Array.isArray(
|
|
98
|
-
const byId = new Map(checks.map((check) => [check.id, check]));
|
|
97
|
+
if (!Array.isArray(requiredValues)) throw checkError("E_CHECK_INVALID", "required values must be an array");
|
|
99
98
|
const errors = [];
|
|
100
|
-
for (const
|
|
101
|
-
const
|
|
99
|
+
for (const value of requiredValues) {
|
|
100
|
+
const candidates = checks.filter((check) => selector(check) === value);
|
|
101
|
+
const check = candidates.find((candidate) => candidate.status === "passed"
|
|
102
|
+
&& (allowInferred || candidate.evidenceKind === "OBSERVED"))
|
|
103
|
+
?? candidates.find((candidate) => candidate.status === "passed")
|
|
104
|
+
?? candidates[0];
|
|
102
105
|
if (!check) {
|
|
103
|
-
errors.push(checkError("E_EVIDENCE_REQUIRED", `Required check is missing: ${
|
|
106
|
+
errors.push(checkError("E_EVIDENCE_REQUIRED", `Required check is missing: ${value}`, [value]));
|
|
104
107
|
continue;
|
|
105
108
|
}
|
|
106
109
|
if (check.status !== "passed") {
|
|
107
|
-
errors.push(checkError("E_EVIDENCE_REQUIRED", `Required check is not passed: ${
|
|
110
|
+
errors.push(checkError("E_EVIDENCE_REQUIRED", `Required check is not passed: ${value}`, [value]));
|
|
108
111
|
continue;
|
|
109
112
|
}
|
|
110
113
|
if (!allowInferred && check.evidenceKind !== "OBSERVED") {
|
|
111
|
-
errors.push(checkError("E_EVIDENCE_KIND_INVALID", `Required check must be observed: ${
|
|
114
|
+
errors.push(checkError("E_EVIDENCE_KIND_INVALID", `Required check must be observed: ${value}`, [value]));
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
117
|
return errors;
|
|
115
118
|
}
|
|
119
|
+
|
|
120
|
+
export function requiredChecksSatisfied(checks, requiredIds, options = {}) {
|
|
121
|
+
return requiredChecksSatisfiedBy(checks, requiredIds, (check) => check.id, options);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function requiredChecksSatisfiedForRequirements(checks, requiredRequirements, options = {}) {
|
|
125
|
+
return requiredChecksSatisfiedBy(checks, requiredRequirements, (check) => check.requirement, options);
|
|
126
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ARTIFACT_PATHS, canonicalFingerprint } from "./artifacts.js";
|
|
2
|
-
import { assertCheckList,
|
|
2
|
+
import { assertCheckList, requiredChecksSatisfiedForRequirements } from "./checks.js";
|
|
3
3
|
import { assertCoverageList, coverageForRequirements } from "./coverage.js";
|
|
4
4
|
import { assertEvidenceList } from "./evidence.js";
|
|
5
5
|
|
|
@@ -86,7 +86,7 @@ export function completionRelationshipErrors({
|
|
|
86
86
|
addAssertion(errors, () => assertCheckList(receipt.checks, "receipt.checks"), "E_CHECK_INVALID", [ARTIFACT_PATHS.receipt]);
|
|
87
87
|
addAssertion(errors, () => assertEvidenceList(receipt.evidence ?? [], "receipt.evidence"), "E_EVIDENCE_INVALID", [ARTIFACT_PATHS.receipt]);
|
|
88
88
|
if (requireRequiredChecks) {
|
|
89
|
-
for (const error of
|
|
89
|
+
for (const error of requiredChecksSatisfiedForRequirements(receipt.checks, requiredEvidence)) {
|
|
90
90
|
errors.push(issue(error.code, error.message, [ARTIFACT_PATHS.receipt]));
|
|
91
91
|
}
|
|
92
92
|
}
|