shapeup-sdlc 3.9.0 → 3.9.2
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.9.
|
|
4
|
+
"version": "3.9.2",
|
|
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/kernel/lib/contract.mjs
CHANGED
|
@@ -263,6 +263,28 @@ function splitList(body) {
|
|
|
263
263
|
* @returns {{meta:Object<string,*>, body:string}} Parsed scalars/lists and the remaining body
|
|
264
264
|
* ({}/whole document when there is no leading `---` block).
|
|
265
265
|
*/
|
|
266
|
+
/**
|
|
267
|
+
* Whether a flow sequence is closed: every `[` outside a quoted run has met its `]`.
|
|
268
|
+
*
|
|
269
|
+
* Quotes are tracked because a bracket inside a quoted path is a character, not structure — and a
|
|
270
|
+
* scope substrate glob is exactly the kind of value that can carry one.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} text - The accumulated text, starting at the opening bracket.
|
|
273
|
+
* @returns {boolean} True once depth returns to zero, i.e. the sequence is complete.
|
|
274
|
+
*/
|
|
275
|
+
function flowClosed(text) {
|
|
276
|
+
let depth = 0;
|
|
277
|
+
let quote = null;
|
|
278
|
+
for (let i = 0; i < text.length; i++) {
|
|
279
|
+
const ch = text[i];
|
|
280
|
+
if (quote) { if (ch === quote && text[i - 1] !== "\\") quote = null; continue; }
|
|
281
|
+
if (ch === '"' || ch === "'") { quote = ch; continue; }
|
|
282
|
+
if (ch === "[") depth++;
|
|
283
|
+
else if (ch === "]") { depth--; if (depth === 0) return true; }
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
266
288
|
export function splitFrontmatter(md) {
|
|
267
289
|
const m = String(md ?? "").match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
268
290
|
if (!m) return { meta: {}, body: String(md ?? "") };
|
|
@@ -275,7 +297,29 @@ export function splitFrontmatter(md) {
|
|
|
275
297
|
const c = line.indexOf(":");
|
|
276
298
|
if (c === -1) continue;
|
|
277
299
|
const key = line.slice(0, c).trim();
|
|
278
|
-
|
|
300
|
+
let inline = line.slice(c + 1).trim();
|
|
301
|
+
|
|
302
|
+
// A FLOW SEQUENCE MAY SPAN LINES, and it used to parse as the single character "[".
|
|
303
|
+
//
|
|
304
|
+
// `field: [` followed by one quoted member per line and a closing `]` is the form a model
|
|
305
|
+
// reaches for the moment a list is too long to read on one line, and this dialect took only
|
|
306
|
+
// the text after the colon — so the value became the string "[", the members were counted as
|
|
307
|
+
// unreadable strays, and every reader downstream saw a string where an array was declared.
|
|
308
|
+
// Measured on a live run: three of five scope contracts were written this way, and each one
|
|
309
|
+
// then failed its own envelope schema at compile — the run could not dispatch those scopes at
|
|
310
|
+
// all. Accepted now: the lines are joined until the brackets balance and the whole thing is
|
|
311
|
+
// coerced as the list it is. A sequence that never closes is still reported, never guessed at.
|
|
312
|
+
if (inline.startsWith("[") && !flowClosed(inline)) {
|
|
313
|
+
let buf = inline;
|
|
314
|
+
let k = i + 1;
|
|
315
|
+
for (; k < lines.length && !flowClosed(buf); k++) buf += " " + lines[k].trim();
|
|
316
|
+
i = k - 1;
|
|
317
|
+
if (flowClosed(buf)) { meta[key] = coerce(buf); continue; }
|
|
318
|
+
meta[key] = null;
|
|
319
|
+
unreadable.push({ field: key, expected_heading: `${key}: [a, b] (or an indented \`- item\` list)`,
|
|
320
|
+
found_under: "a flow sequence that is never closed", rows: k - (i + 1) });
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
279
323
|
|
|
280
324
|
// An indented run beneath a key is a YAML BLOCK SEQUENCE, and it used to be skipped
|
|
281
325
|
// entirely — so `e2e_verification_fixtures:` followed by two `- "node …"` lines parsed to
|
package/kernel/verify/trace.mjs
CHANGED
|
@@ -126,7 +126,9 @@ const IMPORT_RE = /(?:\bimport\b[^'"]*?from\s*|\bimport\s*|\bexport\b[^'"]*?from
|
|
|
126
126
|
* walk uses, plus what each widening contributed (reported, so a reader can see why it resolved).
|
|
127
127
|
*/
|
|
128
128
|
export function sourceExtensions(profile, entryPoint) {
|
|
129
|
-
|
|
129
|
+
// One spelling, the one the schema declares. A silent alias is a field nobody documented and
|
|
130
|
+
// nobody can be told to write.
|
|
131
|
+
const raw = profile?.source_extensions ?? null;
|
|
130
132
|
const list = Array.isArray(raw) ? raw : typeof raw === "string" ? raw.split(/[,\s]+/) : [];
|
|
131
133
|
const declared = list
|
|
132
134
|
.map((e) => String(e).trim())
|
|
@@ -292,7 +294,6 @@ function sourceFilesUnder(root, exts) {
|
|
|
292
294
|
let entries;
|
|
293
295
|
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
294
296
|
for (const e of entries) {
|
|
295
|
-
if (e.name.startsWith(".") && e.name !== ".") { if (SKIP_DIRS.has(e.name)) continue; }
|
|
296
297
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
297
298
|
const abs = join(dir, e.name);
|
|
298
299
|
if (e.isDirectory()) walk(abs);
|
|
@@ -319,7 +320,13 @@ export function scopeReachability(contracts, reachable, sources) {
|
|
|
319
320
|
for (const found of contracts) {
|
|
320
321
|
const c = found?.contract || found || {};
|
|
321
322
|
const id = c.scope_id || found?.id;
|
|
322
|
-
|
|
323
|
+
// A CONTRACT THIS CANNOT READ IS SKIPPED, NEVER THROWN OVER. The whole oracle runs advisory, so
|
|
324
|
+
// a throw here takes out covers-closure too and the run gets no report at all — one malformed
|
|
325
|
+
// contract silencing every arm. A substrate that is not a list of globs is simply a scope this
|
|
326
|
+
// arm has nothing to say about.
|
|
327
|
+
const declared = c.allowed_file_substrate;
|
|
328
|
+
if (!Array.isArray(declared) || !declared.length) continue;
|
|
329
|
+
const globs = declared.filter((g) => typeof g === "string").map(globToRegExp);
|
|
323
330
|
if (!globs.length) continue;
|
|
324
331
|
const owned = sources.filter((f) => globs.some((r) => r.test(f)));
|
|
325
332
|
if (!owned.length) continue;
|
|
@@ -335,8 +342,10 @@ export function scopeReachability(contracts, reachable, sources) {
|
|
|
335
342
|
* @param {{cwd:string, gate?:boolean}} opts - cwd (root the SHARED/LOCAL paths resolve against),
|
|
336
343
|
* gate (records mode; the CLI, not this function, turns gate+red into a non-zero exit).
|
|
337
344
|
* @returns {{report:object, mermaid:(string|null)}} The trace report (covers_closure, reachability,
|
|
338
|
-
* findings[], overall "green"|"red") and a Mermaid view when a wiring map
|
|
339
|
-
* self-skips (checked=false) when its artifact is absent — non-regression on
|
|
345
|
+
* scope_reachability, findings[], overall "green"|"red") and a Mermaid view when a wiring map
|
|
346
|
+
* exists. Each arm self-skips (checked=false) when its artifact is absent — non-regression on
|
|
347
|
+
* pre-spine specs — and reachability also self-skips when it cannot root its walk, which is what
|
|
348
|
+
* skips the per-scope arm with it.
|
|
340
349
|
*/
|
|
341
350
|
export function traceLint(slug, { cwd, gate = false }) {
|
|
342
351
|
const shared = sharedRoot(cwd, slug);
|
|
@@ -491,7 +500,8 @@ export function traceLint(slug, { cwd, gate = false }) {
|
|
|
491
500
|
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
501
|
findings.push({ severity: "warn", code: "REACH-NO-CONTROL", message:
|
|
493
502
|
`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
|
|
503
|
+
"Every engine orphaned is the one result this arm cannot distinguish from a wrong root. If some module does compose the app, declare that one as the entry point. " +
|
|
504
|
+
"If the screens are registered in a manifest instead — a route map, a plugin table — then no entry point roots this walk and unchecked is the correct end state for this project, not a profile to fix." });
|
|
495
505
|
} else {
|
|
496
506
|
for (const u of unreachable) {
|
|
497
507
|
findings.push({ severity: "red", code: "UC-UNREACHABLE", uc: u.use_case,
|
package/package.json
CHANGED
|
@@ -224,10 +224,12 @@ Do NOT enter MAP SCOPES until Orient is accepted.
|
|
|
224
224
|
archetype ∈ {client-only-game|web-service|mobile|library|data-pipeline}; entry_point is the
|
|
225
225
|
reachability seam (a game's main.js is NOT a service's src/server.ts). Validate the enum — a
|
|
226
226
|
typo must fail, not silently disable the check. entry_point is also the root reachability walks
|
|
227
|
-
the import graph from, so
|
|
228
|
-
platform
|
|
229
|
-
string-loaded page)
|
|
230
|
-
|
|
227
|
+
the import graph from, so where some module does compose the app, name that one rather than the
|
|
228
|
+
file the platform happens to start. Where the screens are registered in a MANIFEST instead (a
|
|
229
|
+
route map, a plugin table, a string-loaded page), no entry point roots an import walk at all —
|
|
230
|
+
measured on a real ArkTS app, where the navigation host imports nothing and every screen arrives
|
|
231
|
+
through route_map.json. The arm then reports that it could not check, and that is the correct
|
|
232
|
+
end state for such a project rather than a profile to keep re-declaring.
|
|
231
233
|
source_extensions is optional and only needed when the project's modules end in something the
|
|
232
234
|
entry point does not (e.g. [".ets"] when the entry is a .ts file); leaving it out costs a
|
|
233
235
|
skipped arm, never a false red. The two probes feed the round build gate (`harness verify
|
|
@@ -1800,6 +1800,13 @@ while (verdict !== "pass" && round <= maxRounds) {
|
|
|
1800
1800
|
`${gate.detail ? `: ${gate.detail}` : ""}). Treated as undeclared, not as green.`);
|
|
1801
1801
|
}
|
|
1802
1802
|
|
|
1803
|
+
// Trace-lint runs a SECOND time here, and this is the pass that can see anything. At L1b the
|
|
1804
|
+
// scopes' files do not exist yet, so per-scope reachability measures an empty set and its warning
|
|
1805
|
+
// cannot fire. After the round's build gate the code is on disk, which is exactly when "this
|
|
1806
|
+
// scope's fixture is green and none of its code is in the compiled set" becomes a checkable fact
|
|
1807
|
+
// — the shape that once left three scopes green while their files never compiled. Advisory, like
|
|
1808
|
+
// the L1b pass: it overwrites the same projection with a later and better-informed one.
|
|
1809
|
+
await advisory(`verify trace --slug ${slug} --quiet`, "Build", `trace-lint:r${round}`);
|
|
1803
1810
|
await advisory(`reduce hill --slug ${slug}`, "Build", "hill-derive");
|
|
1804
1811
|
{
|
|
1805
1812
|
const g = await crossGate("L2", "Build", ["proceed", "ask", "abort"],
|