shapeup-sdlc 3.8.0 → 3.9.0
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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shapeup-sdlc-plugin",
|
|
3
3
|
"displayName": "ShapeUp SDLC Plugin",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.9.0",
|
|
5
5
|
"description": "Shape Up SDLC harness for Claude Code: shaping, intake, orient, scope-mapping, building (T0-verified, sandboxed, scope-contracted), evaluation and QA skills orchestrated by a tech-lead.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Liberty Nguyen",
|
package/README.md
CHANGED
|
@@ -139,7 +139,7 @@ rest of this README after this table and nothing will be a surprise.
|
|
|
139
139
|
| **hill / hill phase** | How much of a scope is still *unknown* versus merely *unfinished*. Derived from T0 facts — never self-reported. |
|
|
140
140
|
| **gate (L0–L4)** | A numbered checkpoint in a run. Most pause for you; GATE L2 is the one a hook observes and reports on. |
|
|
141
141
|
| **covers-closure** | Every requirement clause has at least one task claiming to cover it. Nothing silently drops. |
|
|
142
|
-
| **wiring reachability** | Every engine has a call site reachable from the app's real entry point. Catches "built, but never wired up". |
|
|
142
|
+
| **wiring reachability** | Every engine has a call site reachable from the app's real entry point. Catches "built, but never wired up". Reports itself unchecked, with a reason, when the import walk cannot be rooted — an unfollowable import, or no reachable engine to control it. |
|
|
143
143
|
| **discovery ledger** | The one file everything found mid-run gets written to, so nothing is lost between rounds. |
|
|
144
144
|
|
|
145
145
|
A longer version, including the internals, is in [docs/glossary.md](docs/glossary.md).
|
|
@@ -316,7 +316,10 @@ These hold across the harness and are the reason it stays predictable:
|
|
|
316
316
|
`harness reduce ingest`; workers return data and never touch shared state.
|
|
317
317
|
- **Traceability is oracle-checked, opt-in** — `harness verify trace` verifies covers-closure and
|
|
318
318
|
wiring reachability from the committed spine artifacts; it ships advisory (warn-only) and every
|
|
319
|
-
arm is skipped when its artifact is absent, so older specs are non-regressed.
|
|
319
|
+
arm is skipped when its artifact is absent, so older specs are non-regressed. Reachability also
|
|
320
|
+
skips when it cannot root its walk — an import it cannot follow, or no reachable engine to
|
|
321
|
+
control the result — because "every engine is orphaned" and "this is the wrong entry point" are
|
|
322
|
+
the same evidence, and a check that cannot tell them apart must say so rather than pick.
|
|
320
323
|
|
|
321
324
|
## Known rough edges
|
|
322
325
|
|
|
@@ -2287,6 +2287,11 @@
|
|
|
2287
2287
|
"type": "string",
|
|
2288
2288
|
"description": "Optional context on the archetype/entry-point choice."
|
|
2289
2289
|
},
|
|
2290
|
+
"source_extensions": {
|
|
2291
|
+
"type": "array",
|
|
2292
|
+
"items": { "type": "string" },
|
|
2293
|
+
"description": "OPTIONAL — the file extensions this project's modules use, for the import walk that reachability runs. Absent means the JS/TS family plus the entry point's own suffix, which is right for most stacks and wrong for any stack whose engines end in something the entry point does not. Declare it when they differ (e.g. [\".ets\"] for ArkTS, [\".vue\", \".ts\"] for a Vue app): a walk that cannot follow an edge reports itself unchecked rather than reporting every engine orphaned, so the cost of leaving this absent is a skipped arm, never a false red."
|
|
2294
|
+
},
|
|
2290
2295
|
"build_probe": {
|
|
2291
2296
|
"type": "string",
|
|
2292
2297
|
"description": "OPTIONAL — a command that asserts the BUILT ARTIFACT, not the build's exit code, and exits 0 only when it holds. Exists because a green build is not proof the feature compiled: some toolchains compile only the files reachable from an entry point, so a scope's new files can sit outside the compiled set while the build stays green (measured: an app package holding 3 compiled files, 58 errors once the rest became reachable). Archetype-specific by construction — e.g. 'the compiled source map lists every file under each scope's substrate'. Run by harness verify build after run_cmd, once per round before EVAL; absent = no such step, never a failure."
|
package/kernel/verify/trace.mjs
CHANGED
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
// `entry_point` via the import graph (0 import sites) is RED. This catches the *dead module*
|
|
13
13
|
// (631 lines, 26 passing tests, zero call sites), not a *dead data-path* (§2 honest boundary
|
|
14
14
|
// → §4.4). Entry point is PROFILE-GATED, never hardcoded (main.js for a game is not the seam
|
|
15
|
-
// for a web-service).
|
|
15
|
+
// for a web-service). It reports `checked: false` with a reason rather than a verdict when it
|
|
16
|
+
// cannot root the walk: an import it could not follow (the graph is missing edges, so nothing
|
|
17
|
+
// about a destination follows from not arriving there), or no reachable engine at all (with
|
|
18
|
+
// no positive control, an orphaned module and a wrong entry point are the same evidence —
|
|
19
|
+
// and a framework that registers screens by name produces the second on every run).
|
|
16
20
|
//
|
|
17
21
|
// Governing rule: if a script can't check it, it's decoration. This script checks a deletion and
|
|
18
22
|
// an orphan — both provable from files, zero LLM tokens. What it deliberately does NOT assert:
|
|
@@ -39,8 +43,9 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSy
|
|
|
39
43
|
import { resolve, join, dirname, relative, isAbsolute } from "node:path";
|
|
40
44
|
import { readBoard } from "../compile.mjs";
|
|
41
45
|
import { runArgs } from "../lib/argv.mjs";
|
|
42
|
-
import { sharedRoot, traceDir, relLocal } from "../lib/paths.mjs";
|
|
43
|
-
import {
|
|
46
|
+
import { sharedRoot, traceDir, relLocal, scopesDir } from "../lib/paths.mjs";
|
|
47
|
+
import { globToRegExp } from "./spec.mjs";
|
|
48
|
+
import { readContract, readAllContracts, unreadableReason, LEGACY_LAYOUT, WIRING_MAP, PROJECT_PROFILE, SCOPE_CONTRACT, reqId } from "../lib/contract.mjs";
|
|
44
49
|
|
|
45
50
|
// --- requirements.md registry parser -----------------------------------------
|
|
46
51
|
// A committed markdown table: | REQ-id | clause (verbatim) | source | status | note |
|
|
@@ -97,37 +102,81 @@ export function coveredReqIds(board) {
|
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
// --- import-graph reachability -----------------------------------------------
|
|
100
|
-
|
|
105
|
+
//
|
|
106
|
+
// THE RESOLVER KNOWS ONE FAMILY OF LANGUAGES, AND IT MUST SAY SO. This list is the JS/TS family and
|
|
107
|
+
// nothing else, which is correct for the stacks it was written against and silently wrong for any
|
|
108
|
+
// other: a stack whose modules end in something else resolves no relative import at all, the walk
|
|
109
|
+
// stops at the entry file, and every engine then looks "never imported from the entry point" — a
|
|
110
|
+
// red verdict on every input, reported as a check that ran. Two things keep that from happening:
|
|
111
|
+
// the extension set is widened from what the project actually declares (below), and a walk that
|
|
112
|
+
// could not follow an edge reports itself unchecked instead of reporting the destination missing.
|
|
113
|
+
const DEFAULT_SOURCE_EXTS = [".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx"];
|
|
101
114
|
const IMPORT_RE = /(?:\bimport\b[^'"]*?from\s*|\bimport\s*|\bexport\b[^'"]*?from\s*|\brequire\s*\(\s*|\bimport\s*\()\s*['"]([^'"]+)['"]/g;
|
|
102
115
|
|
|
116
|
+
/**
|
|
117
|
+
* The module extensions this project's import graph is walked with.
|
|
118
|
+
*
|
|
119
|
+
* Widened two ways, both from declarations rather than from a guess: the project profile may state
|
|
120
|
+
* `source_extensions` outright, and the entry point's own suffix is always a module extension of
|
|
121
|
+
* this project by construction — it is the one file the profile names and the walk starts from.
|
|
122
|
+
*
|
|
123
|
+
* @param {(object|null)} profile - The parsed ProjectProfile, or null.
|
|
124
|
+
* @param {(string|null)} entryPoint - The declared entry-point path, or null.
|
|
125
|
+
* @returns {{exts:string[], declared:string[], from_entry:(string|null)}} The extension set the
|
|
126
|
+
* walk uses, plus what each widening contributed (reported, so a reader can see why it resolved).
|
|
127
|
+
*/
|
|
128
|
+
export function sourceExtensions(profile, entryPoint) {
|
|
129
|
+
const raw = profile?.source_extensions ?? profile?.module_extensions ?? null;
|
|
130
|
+
const list = Array.isArray(raw) ? raw : typeof raw === "string" ? raw.split(/[,\s]+/) : [];
|
|
131
|
+
const declared = list
|
|
132
|
+
.map((e) => String(e).trim())
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
.map((e) => (e.startsWith(".") ? e : "." + e));
|
|
135
|
+
const m = /(\.[A-Za-z0-9]+)$/.exec(entryPoint || "");
|
|
136
|
+
const fromEntry = m ? m[1] : null;
|
|
137
|
+
const exts = [...new Set([...DEFAULT_SOURCE_EXTS, ...declared, ...(fromEntry ? [fromEntry] : [])])];
|
|
138
|
+
return { exts, declared, from_entry: fromEntry };
|
|
139
|
+
}
|
|
140
|
+
|
|
103
141
|
/**
|
|
104
142
|
* Resolve a relative import specifier to a repo-relative source file.
|
|
143
|
+
*
|
|
144
|
+
* The three answers are kept apart on purpose. A bare specifier is out of the app graph by design;
|
|
145
|
+
* a relative specifier carrying a non-module suffix (`./styles.css`, `./data.json`) is an asset,
|
|
146
|
+
* which imports nothing and can never be an engine; and a relative specifier that looks like a
|
|
147
|
+
* module but resolves to no file is an edge the walk could not follow — the one case that makes
|
|
148
|
+
* the resulting graph incomplete, and the caller has to be able to see it.
|
|
149
|
+
*
|
|
105
150
|
* @param {string} fromFileAbs - Absolute path of the importing file.
|
|
106
151
|
* @param {string} spec - The import specifier string.
|
|
107
152
|
* @param {string} cwd - Repo root the result is made relative to.
|
|
108
|
-
* @
|
|
109
|
-
*
|
|
153
|
+
* @param {string[]} exts - The module extensions of this project (see `sourceExtensions`).
|
|
154
|
+
* @returns {{file:(string|null), kind:("bare"|"asset"|"resolved"|"unresolved")}} The repo-relative
|
|
155
|
+
* source path when one was found, and which of the four answers this was.
|
|
110
156
|
*/
|
|
111
|
-
function resolveSpecifier(fromFileAbs, spec, cwd) {
|
|
112
|
-
if (!spec.startsWith(".")) return null
|
|
157
|
+
function resolveSpecifier(fromFileAbs, spec, cwd, exts) {
|
|
158
|
+
if (!spec.startsWith(".")) return { file: null, kind: "bare" }; // node_modules, out of the app graph
|
|
159
|
+
const suffix = /(\.[A-Za-z0-9]+)$/.exec(spec);
|
|
113
160
|
const baseAbs = resolve(dirname(fromFileAbs), spec);
|
|
114
|
-
const candidates = [baseAbs, ...
|
|
161
|
+
const candidates = [baseAbs, ...exts.map((e) => baseAbs + e), ...exts.map((e) => join(baseAbs, "index" + e))];
|
|
115
162
|
for (const c of candidates) {
|
|
116
|
-
if (existsSync(c) && statSync(c).isFile()) return relative(cwd, c).split("\\").join("/");
|
|
163
|
+
if (existsSync(c) && statSync(c).isFile()) return { file: relative(cwd, c).split("\\").join("/"), kind: "resolved" };
|
|
117
164
|
}
|
|
118
|
-
return null;
|
|
165
|
+
if (suffix && !exts.includes(suffix[1])) return { file: null, kind: "asset" };
|
|
166
|
+
return { file: null, kind: "unresolved" };
|
|
119
167
|
}
|
|
120
168
|
|
|
121
169
|
/**
|
|
122
170
|
* Normalize a declared path (entry_point / engine) to an existing repo-relative source file.
|
|
123
171
|
* @param {string} p - The declared path (absolute or cwd-relative).
|
|
124
172
|
* @param {string} cwd - Repo root the result is made relative to.
|
|
173
|
+
* @param {string[]} [exts] - The module extensions to try (defaults to the JS/TS family).
|
|
125
174
|
* @returns {(string|null)} The repo-relative source path (trying source extensions and `/index`),
|
|
126
175
|
* or null when nothing on disk matches.
|
|
127
176
|
*/
|
|
128
|
-
function resolveFile(p, cwd) {
|
|
177
|
+
function resolveFile(p, cwd, exts = DEFAULT_SOURCE_EXTS) {
|
|
129
178
|
const abs = isAbsolute(p) ? p : resolve(cwd, p);
|
|
130
|
-
const candidates = [abs, ...
|
|
179
|
+
const candidates = [abs, ...exts.map((e) => abs + e), ...exts.map((e) => join(abs, "index" + e))];
|
|
131
180
|
for (const c of candidates) {
|
|
132
181
|
if (existsSync(c) && statSync(c).isFile()) return relative(cwd, c).split("\\").join("/");
|
|
133
182
|
}
|
|
@@ -149,27 +198,37 @@ function importsOf(fileAbs) {
|
|
|
149
198
|
|
|
150
199
|
/**
|
|
151
200
|
* BFS the import graph from an entry point.
|
|
201
|
+
*
|
|
202
|
+
* Reports the edges it could NOT follow alongside the set it built. "This module is never imported"
|
|
203
|
+
* is only a supportable claim over a graph with every edge in it; over a graph missing edges it is
|
|
204
|
+
* indistinguishable from "the walker could not read this language", and the second must not be
|
|
205
|
+
* published as the first.
|
|
206
|
+
*
|
|
152
207
|
* @param {string} entryRel - The entry-point path (declared form; resolved on disk).
|
|
153
208
|
* @param {string} cwd - Repo root.
|
|
154
|
-
* @
|
|
155
|
-
*
|
|
156
|
-
*
|
|
209
|
+
* @param {string[]} [exts] - The module extensions of this project (defaults to the JS/TS family).
|
|
210
|
+
* @returns {{reachable:Set<string>, entryResolved:(string|null), unresolved:Array<{from:string,
|
|
211
|
+
* spec:string}>}} The set of repo-relative files reachable from the entry, the resolved entry
|
|
212
|
+
* path (null when the entry is not on disk, in which case `reachable` is empty), and every
|
|
213
|
+
* module-shaped relative specifier that resolved to no file.
|
|
157
214
|
*/
|
|
158
|
-
export function reachableFrom(entryRel, cwd) {
|
|
215
|
+
export function reachableFrom(entryRel, cwd, exts = DEFAULT_SOURCE_EXTS) {
|
|
159
216
|
const reachable = new Set();
|
|
160
|
-
const
|
|
161
|
-
|
|
217
|
+
const unresolved = [];
|
|
218
|
+
const start = resolveFile(entryRel, cwd, exts);
|
|
219
|
+
if (!start) return { reachable, entryResolved: null, unresolved };
|
|
162
220
|
const queue = [start];
|
|
163
221
|
reachable.add(start);
|
|
164
222
|
while (queue.length) {
|
|
165
223
|
const cur = queue.shift();
|
|
166
224
|
const curAbs = resolve(cwd, cur);
|
|
167
225
|
for (const spec of importsOf(curAbs)) {
|
|
168
|
-
const dep = resolveSpecifier(curAbs, spec, cwd);
|
|
226
|
+
const { file: dep, kind } = resolveSpecifier(curAbs, spec, cwd, exts);
|
|
227
|
+
if (kind === "unresolved") unresolved.push({ from: cur, spec });
|
|
169
228
|
if (dep && !reachable.has(dep)) { reachable.add(dep); queue.push(dep); }
|
|
170
229
|
}
|
|
171
230
|
}
|
|
172
|
-
return { reachable, entryResolved: start };
|
|
231
|
+
return { reachable, entryResolved: start, unresolved };
|
|
173
232
|
}
|
|
174
233
|
|
|
175
234
|
// --- Mermaid view (a view of the checked graph, so it cannot drift — §2) ------
|
|
@@ -199,6 +258,76 @@ export function wiringMermaid(wiringMap, unreachableSet) {
|
|
|
199
258
|
return lines.join("\n");
|
|
200
259
|
}
|
|
201
260
|
|
|
261
|
+
// --- per-scope reachability ---------------------------------------------------
|
|
262
|
+
//
|
|
263
|
+
// A SCOPE'S BUILD FIXTURE PROVES NOTHING UNTIL THE SCOPE'S CODE IS REACHABLE, and on a toolchain
|
|
264
|
+
// that compiles only what the entry point reaches, the two come apart in the worst direction:
|
|
265
|
+
// three scopes were T0-green on an assemble fixture while their own files did not compile at all,
|
|
266
|
+
// and the errors surfaced only once a fourth scope — one that may not write those files — wired
|
|
267
|
+
// the screens in. The fixture was honest about what it ran. Nothing asked whether what it ran
|
|
268
|
+
// included the scope's work.
|
|
269
|
+
//
|
|
270
|
+
// This arm asks, and only ever warns. A scope legitimately owns resources, route maps, manifests,
|
|
271
|
+
// tests and files a later scope will wire, so *some* of its substrate sitting outside the import
|
|
272
|
+
// graph is the normal case and says nothing. What is worth a word is a scope with source files on
|
|
273
|
+
// disk and NOT ONE of them reachable: everything that scope contributes is outside the running
|
|
274
|
+
// app, which is the shape the defect had. Red would be wrong even then — the wiring may be the
|
|
275
|
+
// next scope's job by design, which is a plan the PO made, not a defect the oracle found.
|
|
276
|
+
const SKIP_DIRS = new Set([".git", "node_modules", "oh_modules", ".shapeup", "build", "dist", "out", ".idea", "coverage"]);
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* List the repo-relative source files under a root, by module extension.
|
|
280
|
+
* @param {string} root - Repo root; results are relative to it.
|
|
281
|
+
* @param {string[]} exts - The module extensions that make a file a source file.
|
|
282
|
+
* @returns {string[]} Repo-relative paths, build and dependency directories skipped.
|
|
283
|
+
*/
|
|
284
|
+
function sourceFilesUnder(root, exts) {
|
|
285
|
+
const out = [];
|
|
286
|
+
/**
|
|
287
|
+
* Walk one directory, recursing into its subdirectories.
|
|
288
|
+
* @param {string} dir - Absolute directory to walk.
|
|
289
|
+
* @returns {void}
|
|
290
|
+
*/
|
|
291
|
+
const walk = (dir) => {
|
|
292
|
+
let entries;
|
|
293
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
294
|
+
for (const e of entries) {
|
|
295
|
+
if (e.name.startsWith(".") && e.name !== ".") { if (SKIP_DIRS.has(e.name)) continue; }
|
|
296
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
297
|
+
const abs = join(dir, e.name);
|
|
298
|
+
if (e.isDirectory()) walk(abs);
|
|
299
|
+
else if (exts.some((x) => e.name.endsWith(x))) out.push(relative(root, abs).split("\\").join("/"));
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
walk(root);
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* For each scope contract, how much of its own source substrate the app actually reaches.
|
|
308
|
+
*
|
|
309
|
+
* @param {Array<{id?:string, contract?:object}>} contracts - Scope contracts as `readAllContracts`
|
|
310
|
+
* returns them.
|
|
311
|
+
* @param {Set<string>} reachable - The repo-relative files reachable from the entry point.
|
|
312
|
+
* @param {string[]} sources - Every repo-relative source file on disk.
|
|
313
|
+
* @returns {Array<{scope_id:string, source_files:number, reachable_files:number}>} One row per
|
|
314
|
+
* scope that owns at least one source file on disk; scopes owning none are left out entirely,
|
|
315
|
+
* because a scope with nothing to reach is not a finding.
|
|
316
|
+
*/
|
|
317
|
+
export function scopeReachability(contracts, reachable, sources) {
|
|
318
|
+
const rows = [];
|
|
319
|
+
for (const found of contracts) {
|
|
320
|
+
const c = found?.contract || found || {};
|
|
321
|
+
const id = c.scope_id || found?.id;
|
|
322
|
+
const globs = (c.allowed_file_substrate || []).map(globToRegExp);
|
|
323
|
+
if (!globs.length) continue;
|
|
324
|
+
const owned = sources.filter((f) => globs.some((r) => r.test(f)));
|
|
325
|
+
if (!owned.length) continue;
|
|
326
|
+
rows.push({ scope_id: id, source_files: owned.length, reachable_files: owned.filter((f) => reachable.has(f)).length });
|
|
327
|
+
}
|
|
328
|
+
return rows;
|
|
329
|
+
}
|
|
330
|
+
|
|
202
331
|
// --- the oracle --------------------------------------------------------------
|
|
203
332
|
/**
|
|
204
333
|
* Run the covers-closure + reachability oracle for a slug.
|
|
@@ -262,6 +391,10 @@ export function traceLint(slug, { cwd, gate = false }) {
|
|
|
262
391
|
const wiringPath = join(shared, "wiring-map.md");
|
|
263
392
|
const profilePath = join(shared, "project-profile.md");
|
|
264
393
|
let reachability = { checked: false, pass: true, unreachable: [], skipped_reason: "no wiring-map — reachability not applicable." };
|
|
394
|
+
// Hoisted so the per-scope arm below can reuse the walk this one already paid for. Both stay
|
|
395
|
+
// null unless reachability actually ran, which is what gates the second arm on the first.
|
|
396
|
+
let reachableSet = null;
|
|
397
|
+
let walkExts = null;
|
|
265
398
|
let wiringMap = null;
|
|
266
399
|
|
|
267
400
|
let wiringFound = null;
|
|
@@ -308,29 +441,95 @@ export function traceLint(slug, { cwd, gate = false }) {
|
|
|
308
441
|
if (!entryPoint) {
|
|
309
442
|
reachability = { checked: false, pass: true, unreachable: [], skipped_reason: "project-profile has no entry_point." };
|
|
310
443
|
} else {
|
|
311
|
-
const {
|
|
444
|
+
const { exts, declared, from_entry: fromEntry } = sourceExtensions(profile, entryPoint);
|
|
445
|
+
const { reachable, entryResolved, unresolved } = reachableFrom(entryPoint, cwd, exts);
|
|
312
446
|
if (!entryResolved) {
|
|
313
447
|
reachability = { checked: false, pass: true, unreachable: [], entry_point: entryPoint,
|
|
314
448
|
skipped_reason: `entry_point "${entryPoint}" does not resolve to a source file on disk — reachability skipped.` };
|
|
315
449
|
findings.push({ severity: "warn", code: "ENTRY-MISSING", message: `project-profile.md entry_point "${entryPoint}" is not on disk — reachability cannot run.` });
|
|
450
|
+
} else if (unresolved.length) {
|
|
451
|
+
// AN INCOMPLETE GRAPH GRADES NOTHING. Every module-shaped relative import the walker could
|
|
452
|
+
// not follow is a missing edge, and a module is "unreachable" only in the sense that this
|
|
453
|
+
// walk did not get there. Publishing that as red produces a check that is red for every
|
|
454
|
+
// input on a stack the resolver cannot read, while reporting that it looked.
|
|
455
|
+
const sample = unresolved.slice(0, 3).map((u) => `${u.spec} (from ${u.from})`);
|
|
456
|
+
reachability = { checked: false, pass: true, unreachable: [], entry_point: entryPoint,
|
|
457
|
+
entry_resolved: entryResolved, reachable_files: reachable.size,
|
|
458
|
+
module_extensions: exts, unresolved_imports: unresolved.length, unresolved_sample: sample,
|
|
459
|
+
skipped_reason: `${unresolved.length} relative import(s) from the entry point resolve to no file with the extensions this project declares (${exts.join(", ")}) — the import graph is incomplete, so "never imported" is not a claim this walk can support.` };
|
|
460
|
+
findings.push({ severity: "warn", code: "GRAPH-INCOMPLETE", message:
|
|
461
|
+
`reachability did not run: ${unresolved.length} relative import(s) could not be resolved (e.g. ${sample.join("; ")}). ` +
|
|
462
|
+
`The walker tried ${exts.join(", ")}${declared.length ? "" : " — the project profile declares no `source_extensions`, so the set is the JS/TS family plus the entry point's own suffix" + (fromEntry ? ` (${fromEntry})` : "")}. ` +
|
|
463
|
+
"Declare `source_extensions` in project-profile.md to let this arm run." });
|
|
316
464
|
} else {
|
|
465
|
+
const engines = wiringMap.entries || [];
|
|
317
466
|
const unreachable = [];
|
|
318
|
-
for (const e of
|
|
319
|
-
const engResolved = resolveFile(e.engine, cwd);
|
|
320
|
-
|
|
321
|
-
if (!ok) {
|
|
467
|
+
for (const e of engines) {
|
|
468
|
+
const engResolved = resolveFile(e.engine, cwd, exts);
|
|
469
|
+
if (!(engResolved && reachable.has(engResolved))) {
|
|
322
470
|
unreachable.push({ use_case: e.use_case, engine: e.engine, reason: engResolved ? "not imported from the entry point" : "engine file not on disk" });
|
|
323
|
-
findings.push({ severity: "red", code: "UC-UNREACHABLE", uc: e.use_case,
|
|
324
|
-
message: `${e.use_case}: engine "${e.engine}" is ${engResolved ? "never imported from" : "missing under"} entry_point "${entryPoint}" — the module ships orphaned from the running app.` });
|
|
325
471
|
}
|
|
326
472
|
}
|
|
327
|
-
|
|
328
|
-
|
|
473
|
+
// THE ARM NEEDS ONE POSITIVE CONTROL, AND EVERY-ENGINE-ORPHANED IS NOT ONE. What this
|
|
474
|
+
// check was built to catch is the dead module: one engine with no call site among
|
|
475
|
+
// siblings that have them. When NO engine is reachable, nothing demonstrates that this
|
|
476
|
+
// entry point is the root the app actually runs from — and for whole archetypes it is
|
|
477
|
+
// not. A framework that registers screens declaratively reaches them by name at runtime
|
|
478
|
+
// (`loadContent("pages/Index")`, a route map, a manifest), so its entry file imports a
|
|
479
|
+
// handful of modules and no engine, and the import graph is complete and beside the
|
|
480
|
+
// point. "Every engine is dead" and "I am walking the wrong tree" produce identical
|
|
481
|
+
// evidence, so the arm reports that it could not check rather than picking one.
|
|
482
|
+
//
|
|
483
|
+
// THE COST, NAMED: a wiring map with a single engine can no longer red, because its only
|
|
484
|
+
// engine being unreachable is exactly the indistinguishable case. A genuinely orphaned
|
|
485
|
+
// module in a one-use-case feature is therefore reported as unchecked, not as dead. That
|
|
486
|
+
// is the price of never being red for every input on a stack this walk cannot root.
|
|
487
|
+
if (engines.length && unreachable.length === engines.length) {
|
|
488
|
+
reachability = { checked: false, pass: true, unreachable: [], entry_point: entryPoint,
|
|
489
|
+
entry_resolved: entryResolved, module_extensions: exts, reachable_files: reachable.size,
|
|
490
|
+
engines_total: engines.length, engines_reachable: 0,
|
|
491
|
+
skipped_reason: `no engine is reachable from entry_point "${entryPoint}", which reaches ${reachable.size} file(s) — with no reachable engine as a control this walk cannot tell an orphaned module from an entry point that is not the runtime root (declarative routing, a manifest, a string-loaded screen). Reachability skipped.` };
|
|
492
|
+
findings.push({ severity: "warn", code: "REACH-NO-CONTROL", message:
|
|
493
|
+
`reachability did not run: all ${engines.length} engine(s) are unreachable from entry_point "${entryPoint}", which reaches ${reachable.size} file(s). ` +
|
|
494
|
+
"Every engine orphaned is the one result this arm cannot distinguish from a wrong root — if this project wires its screens by name rather than by import, declare the module that does the wiring as the entry point." });
|
|
495
|
+
} else {
|
|
496
|
+
for (const u of unreachable) {
|
|
497
|
+
findings.push({ severity: "red", code: "UC-UNREACHABLE", uc: u.use_case,
|
|
498
|
+
message: `${u.use_case}: engine "${u.engine}" is ${u.reason === "engine file not on disk" ? "missing under" : "never imported from"} entry_point "${entryPoint}" — the module ships orphaned from the running app, while ${engines.length - unreachable.length} other engine(s) reach it.` });
|
|
499
|
+
}
|
|
500
|
+
reachableSet = reachable;
|
|
501
|
+
walkExts = exts;
|
|
502
|
+
reachability = { checked: true, entry_point: entryPoint, entry_resolved: entryResolved,
|
|
503
|
+
module_extensions: exts, reachable_files: reachable.size,
|
|
504
|
+
engines_total: engines.length, engines_reachable: engines.length - unreachable.length,
|
|
505
|
+
unreachable, pass: unreachable.length === 0 };
|
|
506
|
+
}
|
|
329
507
|
}
|
|
330
508
|
}
|
|
331
509
|
}
|
|
332
510
|
}
|
|
333
511
|
|
|
512
|
+
// --- per-scope reachability, gated on the first arm having actually run ---------------------
|
|
513
|
+
let scopeReach = { checked: false, scopes: [], orphaned: [],
|
|
514
|
+
skipped_reason: "reachability did not run, so there is no graph to measure a scope against." };
|
|
515
|
+
if (reachableSet) {
|
|
516
|
+
const contracts = readAllContracts(scopesDir(cwd, slug), SCOPE_CONTRACT);
|
|
517
|
+
if (!contracts.length) {
|
|
518
|
+
scopeReach = { checked: false, scopes: [], orphaned: [], skipped_reason: "no scope contracts on disk." };
|
|
519
|
+
} else {
|
|
520
|
+
const rows = scopeReachability(contracts, reachableSet, sourceFilesUnder(cwd, walkExts));
|
|
521
|
+
const orphaned = rows.filter((r) => r.reachable_files === 0);
|
|
522
|
+
scopeReach = { checked: true, scopes: rows, orphaned: orphaned.map((r) => r.scope_id) };
|
|
523
|
+
for (const r of orphaned) {
|
|
524
|
+
findings.push({ severity: "warn", code: "SCOPE-UNREACHABLE", scope: r.scope_id, message:
|
|
525
|
+
`scope ${r.scope_id}: none of its ${r.source_files} source file(s) is reached from the entry point. ` +
|
|
526
|
+
"A build fixture that compiles only what the entry point reaches can be green while this scope's own " +
|
|
527
|
+
"code never compiles — the errors then surface in whichever scope wires the screens in. Warn, not red: " +
|
|
528
|
+
"the wiring may legitimately be a later scope's job." });
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
334
533
|
const overall = findings.some((f) => f.severity === "red") ? "red" : "green";
|
|
335
534
|
const report = {
|
|
336
535
|
schema_version: 1,
|
|
@@ -340,6 +539,7 @@ export function traceLint(slug, { cwd, gate = false }) {
|
|
|
340
539
|
advisory: !gate,
|
|
341
540
|
covers_closure: coversClosure,
|
|
342
541
|
reachability,
|
|
542
|
+
scope_reachability: scopeReach,
|
|
343
543
|
findings,
|
|
344
544
|
overall,
|
|
345
545
|
};
|
package/package.json
CHANGED
|
@@ -220,10 +220,17 @@ Do NOT enter MAP SCOPES until Orient is accepted.
|
|
|
220
220
|
|
|
221
221
|
```
|
|
222
222
|
1. PROFILE (you write it at L0 — compile-order stays pipeline-blind): SHARED project-profile.md
|
|
223
|
-
= {schema_version:1, archetype, entry_point, build_probe?, launch_probe?}.
|
|
224
|
-
{client-only-game|web-service|mobile|library|data-pipeline}; entry_point is the
|
|
225
|
-
seam (a game's main.js is NOT a service's src/server.ts). Validate the enum — a
|
|
226
|
-
not silently disable the check.
|
|
223
|
+
= {schema_version:1, archetype, entry_point, source_extensions?, build_probe?, launch_probe?}.
|
|
224
|
+
archetype ∈ {client-only-game|web-service|mobile|library|data-pipeline}; entry_point is the
|
|
225
|
+
reachability seam (a game's main.js is NOT a service's src/server.ts). Validate the enum — a
|
|
226
|
+
typo must fail, not silently disable the check. entry_point is also the root reachability walks
|
|
227
|
+
the import graph from, so pick the module the app's screens hang off, not merely the file the
|
|
228
|
+
platform starts: a framework that registers screens by name (a route map, a manifest, a
|
|
229
|
+
string-loaded page) leaves its start file importing nothing the feature touches, and the arm
|
|
230
|
+
then reports that it could not check rather than calling every engine orphaned.
|
|
231
|
+
source_extensions is optional and only needed when the project's modules end in something the
|
|
232
|
+
entry point does not (e.g. [".ets"] when the entry is a .ts file); leaving it out costs a
|
|
233
|
+
skipped arm, never a false red. The two probes feed the round build gate (`harness verify
|
|
227
234
|
build`, every round before EVAL): build_probe asserts the BUILT ARTIFACT covers what the run
|
|
228
235
|
wrote (a green exit code is not proof the feature compiled when the toolchain compiles only what
|
|
229
236
|
an entry point reaches); launch_probe installs, starts and asserts the first screen. A `mobile`
|
|
@@ -241,7 +248,14 @@ Do NOT enter MAP SCOPES until Orient is accepted.
|
|
|
241
248
|
requirements.md registry (atomic REQ clauses, frozen ids).
|
|
242
249
|
4. trace-lint — node "${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs" verify trace --slug <slug>. ADVISORY at L1b:
|
|
243
250
|
covers-closure (every covered REQ named by ≥1 AC's covers:) + reachability (every UC engine
|
|
244
|
-
reaches entry_point). Promote to --gate only once covers: is populated.
|
|
251
|
+
reaches entry_point). Promote to --gate only once covers: is populated. Reachability reports
|
|
252
|
+
checked:false with a reason rather than a verdict in two cases, both warns: an import it could
|
|
253
|
+
not follow (the graph is incomplete) and no engine reachable at all (nothing controls the walk,
|
|
254
|
+
so an orphan and a wrong root look identical). Read either as "re-declare the profile", never
|
|
255
|
+
as a clean arm. When it does run it also reports, per scope, how many of that scope's own source
|
|
256
|
+
files the app reaches, and warns (SCOPE-UNREACHABLE) on a scope it reaches none of — a scope's
|
|
257
|
+
build fixture can be green while its code never compiles, on any toolchain that compiles only
|
|
258
|
+
what the entry point reaches. Warn only: the wiring may be a later scope's job by design.
|
|
245
259
|
```
|
|
246
260
|
|
|
247
261
|
---
|