@metaobjectsdev/cli 0.22.1 → 0.23.1-rc.1
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/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +117 -9
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/lib/requirement-check.d.ts +30 -0
- package/dist/src/lib/requirement-check.d.ts.map +1 -1
- package/dist/src/lib/requirement-check.js +259 -13
- package/dist/src/lib/requirement-check.js.map +1 -1
- package/dist/src/lib/verified-by-scan.d.ts +3 -1
- package/dist/src/lib/verified-by-scan.d.ts.map +1 -1
- package/dist/src/lib/verified-by-scan.js +192 -16
- package/dist/src/lib/verified-by-scan.js.map +1 -1
- package/package.json +10 -10
- package/src/commands/verify.ts +133 -8
- package/src/lib/requirement-check.ts +275 -12
- package/src/lib/verified-by-scan.ts +202 -17
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
REQUIREMENT_MIN_LEVEL,
|
|
32
32
|
REQUIREMENT_MAX_LEVEL,
|
|
33
33
|
REQUIREMENT_LEVEL_MEMBER,
|
|
34
|
+
REQUIREMENT_DISPOSITION_DEFERRED,
|
|
34
35
|
REQUIREMENT_STATUSES_REQUIRING_LIVE_NODES,
|
|
35
36
|
resolveObjectRef,
|
|
36
37
|
didYouMeanHint,
|
|
@@ -57,6 +58,26 @@ export const ERR_REQUIREMENT_L4_NOT_OBJECT = "ERR_REQUIREMENT_L4_NOT_OBJECT";
|
|
|
57
58
|
export const ERR_REQUIREMENT_L5_NOT_MEMBER = "ERR_REQUIREMENT_L5_NOT_MEMBER";
|
|
58
59
|
export const ERR_REQUIREMENT_ARCH_NO_IMPLEMENTERS = "ERR_REQUIREMENT_ARCH_NO_IMPLEMENTERS";
|
|
59
60
|
export const WARN_REQUIREMENT_OBJECT_UNCLAIMED = "WARN_REQUIREMENT_OBJECT_UNCLAIMED";
|
|
61
|
+
export const WARN_REQUIREMENT_DISPOSITION_NOT_APPLICABLE = "WARN_REQUIREMENT_DISPOSITION_NOT_APPLICABLE";
|
|
62
|
+
export const WARN_REQUIREMENT_DEFERRED_UNTRACKED = "WARN_REQUIREMENT_DEFERRED_UNTRACKED";
|
|
63
|
+
export const WARN_REQUIREMENT_NOTHING_IMPLEMENTS = "WARN_REQUIREMENT_NOTHING_IMPLEMENTS";
|
|
64
|
+
|
|
65
|
+
/** Counts behind the summary line `meta verify` prints on every run, clean or
|
|
66
|
+
* not. A clean run that says nothing cannot distinguish "checked, all good"
|
|
67
|
+
* from "checked nothing" — and a ledger that skipped an entire grain reads
|
|
68
|
+
* identically to a complete one. */
|
|
69
|
+
export interface RequirementSummary {
|
|
70
|
+
total: number;
|
|
71
|
+
functional: number;
|
|
72
|
+
architectural: number;
|
|
73
|
+
byStatus: Record<string, number>;
|
|
74
|
+
/** partial or planned with NO disposition recorded — the unreviewed gaps. */
|
|
75
|
+
undecided: number;
|
|
76
|
+
/** deferred entries naming no ticket, so nobody will be reminded. */
|
|
77
|
+
deferredUntracked: number;
|
|
78
|
+
entitiesTotal: number;
|
|
79
|
+
entitiesClaimed: number;
|
|
80
|
+
}
|
|
60
81
|
|
|
61
82
|
/** Severity of the object-coverage gate. Promotion to `"error"` is a one-line
|
|
62
83
|
* flip here, which activates an already-written test rather than requiring new
|
|
@@ -95,6 +116,79 @@ export function splitMemberRef(ref: string): { owner: string; path: string[] } {
|
|
|
95
116
|
return { owner: ref.slice(0, dot), path: ref.slice(dot + 1).split(".") };
|
|
96
117
|
}
|
|
97
118
|
|
|
119
|
+
/** Resolution keys of every root-level object whose `extends` chain reaches
|
|
120
|
+
* `ancestor`. Walks the RESOLVED super pointer rather than the raw string, so a
|
|
121
|
+
* cross-package or dotted reference resolves the same way the loader resolved
|
|
122
|
+
* it — reading `superRef` here would be a second, divergent resolver. */
|
|
123
|
+
function subtypesOf(root: MetaData, ancestor: MetaData): string[] {
|
|
124
|
+
const out: string[] = [];
|
|
125
|
+
for (const cand of root.children()) {
|
|
126
|
+
if (cand.type !== TYPE_OBJECT || cand === ancestor) continue;
|
|
127
|
+
const seen = new Set<MetaData>();
|
|
128
|
+
let cur = cand.superData;
|
|
129
|
+
while (cur !== undefined && !seen.has(cur)) {
|
|
130
|
+
seen.add(cur);
|
|
131
|
+
if (cur === ancestor) { out.push(cand.resolutionKey()); break; }
|
|
132
|
+
cur = cur.superData;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** True when this requirement, or anything nested beneath it, names an
|
|
139
|
+
* implementing node. Subtree-scoped deliberately: an L1 solution that delegates
|
|
140
|
+
* everything to its children implements nothing directly, and flagging that
|
|
141
|
+
* would fire on the correct shape of every tree. */
|
|
142
|
+
function subtreeClaimsAnything(req: MetaRequirement): boolean {
|
|
143
|
+
if (req.implementedBy().length > 0) return true;
|
|
144
|
+
for (const child of req.children()) {
|
|
145
|
+
if (child.type !== TYPE_REQUIREMENT) continue;
|
|
146
|
+
if (subtreeClaimsAnything(child as MetaRequirement)) return true;
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Resolve the owner segment of an `@implementedBy` reference to the node it names.
|
|
153
|
+
*
|
|
154
|
+
* OBJECTS FIRST, through the loader's own resolver, so package-local binding stays the
|
|
155
|
+
* ADR-0042 contract and never a parallel name scan (#228).
|
|
156
|
+
*
|
|
157
|
+
* Then ROOT-LEVEL NON-OBJECT nodes — `template.prompt` and its siblings today. The
|
|
158
|
+
* attribute is documented as naming "the model nodes realising this requirement", and a
|
|
159
|
+
* declared prompt is one: it is the durable artifact a capability like "the game master
|
|
160
|
+
* is told what the party can see" actually lives in. Resolving only objects meant the
|
|
161
|
+
* prompt estate — the thing whose retirement is hardest to see in a model, since a
|
|
162
|
+
* removed prompt leaves no table behind — was the one part of a model that could not
|
|
163
|
+
* carry a status. So L4 means "a declared top-level model node", not "an object".
|
|
164
|
+
*
|
|
165
|
+
* Requirements themselves are excluded: hierarchy is nesting, and a requirement claiming
|
|
166
|
+
* a requirement would be a second, contradictory parent mechanism.
|
|
167
|
+
*/
|
|
168
|
+
function resolveClaimTarget(root: MetaData, owner: string, referrerPkg: string): MetaData | undefined {
|
|
169
|
+
const { node } = resolveObjectRef(root, owner, referrerPkg);
|
|
170
|
+
if (node !== undefined) return node;
|
|
171
|
+
|
|
172
|
+
const candidates = root
|
|
173
|
+
.children()
|
|
174
|
+
.filter((c) => c.type !== TYPE_OBJECT && c.type !== TYPE_REQUIREMENT);
|
|
175
|
+
|
|
176
|
+
// A fully-qualified reference binds exactly, like every other FQN in the model.
|
|
177
|
+
if (owner.includes(PACKAGE_SEPARATOR)) {
|
|
178
|
+
return candidates.find((c) => c.resolutionKey() === owner);
|
|
179
|
+
}
|
|
180
|
+
// A bare reference prefers the referrer's own package, then a root-level node of that
|
|
181
|
+
// bare name. An ambiguous bare name binds NOTHING — same fail-closed rule objects use,
|
|
182
|
+
// because silently picking one of two same-named nodes is how a claim ends up pointing
|
|
183
|
+
// at the wrong thing without anyone noticing.
|
|
184
|
+
const local = referrerPkg === "" ? [] : candidates.filter((c) => c.resolutionKey() === `${referrerPkg}${PACKAGE_SEPARATOR}${owner}`);
|
|
185
|
+
if (local.length === 1) return local[0];
|
|
186
|
+
// Root-level (unpackaged) only, matching resolveObjectRef's own bare fallback. A bare
|
|
187
|
+
// ref must not reach into an arbitrary package just because the name is unique there.
|
|
188
|
+
const bare = candidates.filter((c) => c.name === owner && c.resolutionKey() === owner);
|
|
189
|
+
return bare.length === 1 ? bare[0] : undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
98
192
|
/** Walk dotted member segments by CHILD NAME from an object node. */
|
|
99
193
|
function resolveMember(obj: MetaData, path: string[]): MetaData | undefined {
|
|
100
194
|
let cur: MetaData | undefined = obj;
|
|
@@ -120,6 +214,64 @@ export function collectRequirements(root: MetaData): MetaRequirement[] {
|
|
|
120
214
|
return out;
|
|
121
215
|
}
|
|
122
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Resolution keys of every object claimed by a requirement.
|
|
219
|
+
*
|
|
220
|
+
* SHARED by the gate and the summary deliberately. The two used to compute this
|
|
221
|
+
* separately and drifted: the summary missed the extends-chain propagation the
|
|
222
|
+
* gate applies to an ARCHITECTURAL claim, so a project using the documented
|
|
223
|
+
* BaseEntity pattern got a summary reporting entities as unclaimed while the
|
|
224
|
+
* gate beneath it named none. A summary that contradicts its own diagnostics
|
|
225
|
+
* reads as a measurement and cannot be reconciled with the run that produced it.
|
|
226
|
+
*/
|
|
227
|
+
function claimedObjectKeys(root: MetaData, reqs: MetaRequirement[]): Set<string> {
|
|
228
|
+
const claimed = new Set<string>();
|
|
229
|
+
for (const req of reqs) {
|
|
230
|
+
// A PLANNED requirement never contributes to coverage. Otherwise the
|
|
231
|
+
// cheapest way to clear an unclaimed-entity warning would be to declare an
|
|
232
|
+
// intention — the gate would measure ambition rather than work.
|
|
233
|
+
if (req.isPlanned()) continue;
|
|
234
|
+
// referrerPkg is the requirement's own effective package, so a bare ref
|
|
235
|
+
// binds package-locally under the ADR-0042 contract — the loader's own
|
|
236
|
+
// resolver, never a parallel name scan (#228).
|
|
237
|
+
const referrerPkg = req.package ?? req.fileDefaultPackage ?? "";
|
|
238
|
+
for (const ref of req.implementedBy()) {
|
|
239
|
+
const { owner, path } = splitMemberRef(ref);
|
|
240
|
+
const node = resolveClaimTarget(root, owner, referrerPkg);
|
|
241
|
+
if (node === undefined) continue;
|
|
242
|
+
if (path.length > 0 && resolveMember(node, path) === undefined) continue;
|
|
243
|
+
claimed.add(node.resolutionKey());
|
|
244
|
+
// ARCHITECTURAL claims propagate DOWN the extends chain; functional ones
|
|
245
|
+
// do not. A policy ("every row is addressable") claimed on an abstract
|
|
246
|
+
// BaseEntity genuinely holds for everything extending it — that is what
|
|
247
|
+
// universality means, and without this the documented BaseEntity pattern
|
|
248
|
+
// is worse than not using it. A functional claim is the opposite: it says
|
|
249
|
+
// this entity exists for a REASON, and inheriting a reason from a shared
|
|
250
|
+
// base would mean adding an entity no longer forces anyone to say what it
|
|
251
|
+
// is for. Same mechanism, opposite polarity — as everywhere else in the
|
|
252
|
+
// subtype split.
|
|
253
|
+
if (req.subType === REQUIREMENT_SUBTYPE_ARCHITECTURAL) {
|
|
254
|
+
for (const sub of subtypesOf(root, node)) claimed.add(sub);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return claimed;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The entities object coverage measures — and the same set for the gate and the
|
|
263
|
+
* summary, for the reason given on `claimedObjectKeys`.
|
|
264
|
+
*
|
|
265
|
+
* An ABSTRACT entity is shape, not data: there is no table and no rows, so
|
|
266
|
+
* demanding a capability claim for it is the same category error as demanding
|
|
267
|
+
* one for an object.value. It is exempt for the same reason.
|
|
268
|
+
*/
|
|
269
|
+
function coverableEntities(root: MetaData): MetaData[] {
|
|
270
|
+
return root.children().filter(
|
|
271
|
+
(n) => n.type === TYPE_OBJECT && n.subType === OBJECT_SUBTYPE_ENTITY && !n.isAbstract,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
123
275
|
/**
|
|
124
276
|
* Check the requirement tree against the loaded model.
|
|
125
277
|
*
|
|
@@ -133,20 +285,31 @@ export function checkRequirements(root: MetaData): Diagnostic[] {
|
|
|
133
285
|
const reqs = collectRequirements(root);
|
|
134
286
|
if (reqs.length === 0) return out; // opt-in by declaration — no requirements, nothing to say
|
|
135
287
|
|
|
136
|
-
const claimedObjects =
|
|
288
|
+
const claimedObjects = claimedObjectKeys(root, reqs);
|
|
137
289
|
|
|
138
290
|
for (const req of reqs) {
|
|
139
291
|
const architectural = req.subType === REQUIREMENT_SUBTYPE_ARCHITECTURAL;
|
|
140
292
|
const level = req.level();
|
|
141
293
|
const refs = req.implementedBy();
|
|
142
294
|
|
|
143
|
-
|
|
144
|
-
|
|
295
|
+
// -- the level rules -------------------------------------------------------
|
|
296
|
+
// A functional requirement MUST be levelled. An architectural one MAY be,
|
|
297
|
+
// and levelling is the OPT-IN: unlevelled it is the original flat,
|
|
298
|
+
// object-independent policy and these rules must not touch it. Once a level
|
|
299
|
+
// is present the node has joined a tree, and `@level`'s own registered
|
|
300
|
+
// description promises that "the same rules as functional apply: nesting
|
|
301
|
+
// must agree with the level". Enforcing that here is what makes the promise
|
|
302
|
+
// true — a levelled architectural node used to be exempt from BOTH checks,
|
|
303
|
+
// so an ISO-25010 tree could re-ascend or declare a level 7 in silence.
|
|
304
|
+
const levelled = level !== undefined;
|
|
305
|
+
if (!architectural || levelled) {
|
|
306
|
+
if (!levelled || !Number.isInteger(level)
|
|
145
307
|
|| level < REQUIREMENT_MIN_LEVEL || level > REQUIREMENT_MAX_LEVEL) {
|
|
146
308
|
out.push({
|
|
147
309
|
severity: "error", code: ERR_REQUIREMENT_BAD_LEVEL, name: req.name,
|
|
148
310
|
message: `level must be an integer ${REQUIREMENT_MIN_LEVEL}-${REQUIREMENT_MAX_LEVEL} (got ${String(level)}). ` +
|
|
149
|
-
`L1 solution, L2 segment (app/library), L3 service, L4 object, L5 member
|
|
311
|
+
`L1 solution, L2 segment (app/library), L3 service, L4 object, L5 member.` +
|
|
312
|
+
(architectural ? ` On an architectural requirement the level is optional — omit it for a flat policy.` : ``),
|
|
150
313
|
});
|
|
151
314
|
}
|
|
152
315
|
// Nesting IS the hierarchy, so a child must sit strictly below its parent.
|
|
@@ -180,13 +343,19 @@ export function checkRequirements(root: MetaData): Diagnostic[] {
|
|
|
180
343
|
// binds package-locally under the ADR-0042 contract — the loader's own
|
|
181
344
|
// resolver, never a parallel name scan (#228).
|
|
182
345
|
const referrerPkg = req.package ?? req.fileDefaultPackage ?? "";
|
|
183
|
-
const
|
|
346
|
+
const node = resolveClaimTarget(root, owner, referrerPkg);
|
|
184
347
|
const isObjectRef = path.length === 0;
|
|
185
348
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
349
|
+
// GRAIN, and it stays functional-only DELIBERATELY. On a functional
|
|
350
|
+
// requirement L4 and L5 MEAN "an object" and "a member" — that is what the
|
|
351
|
+
// allocation step allocates. On a levelled architectural one the upper
|
|
352
|
+
// tiers are a quality taxonomy and L4/L5 retain only their link-floor
|
|
353
|
+
// meaning, so a policy whose claim set legitimately mixes grains ("every
|
|
354
|
+
// money FIELD declares its currency", claimed alongside the entities that
|
|
355
|
+
// hold them) must not be forced to split by grain to say so. Extending
|
|
356
|
+
// this to architectural would be a new rule, not the missing half of an
|
|
357
|
+
// existing one — unlike the level checks above, which `@level` already
|
|
358
|
+
// promised.
|
|
190
359
|
if (!architectural && level === REQUIREMENT_LINK_FLOOR_LEVEL && !isObjectRef) {
|
|
191
360
|
out.push({
|
|
192
361
|
severity: "error", code: ERR_REQUIREMENT_L4_NOT_OBJECT, name: req.name,
|
|
@@ -227,13 +396,59 @@ export function checkRequirements(root: MetaData): Diagnostic[] {
|
|
|
227
396
|
const status = req.status();
|
|
228
397
|
const live = status !== undefined
|
|
229
398
|
&& REQUIREMENT_STATUSES_REQUIRING_LIVE_NODES.includes(status as RequirementStatus);
|
|
230
|
-
|
|
399
|
+
// Two exemptions, both structural rather than lenient.
|
|
400
|
+
//
|
|
401
|
+
// `planned` — a policy that is not built yet is SUPPOSED to be applied to
|
|
402
|
+
// nothing, so the check would fire on precisely the entries it should stay
|
|
403
|
+
// quiet about.
|
|
404
|
+
//
|
|
405
|
+
// An ORGANISATIONAL node in a levelled architectural tree — an "ISO 25010
|
|
406
|
+
// Security" at L1 delegates to its children and names nothing, exactly as
|
|
407
|
+
// an L1 functional node does. mayReferenceModel() is the right predicate
|
|
408
|
+
// because it already encodes "is this tier allowed to name the model at
|
|
409
|
+
// all": true for a flat policy (the original form), false for L1-L3 of a
|
|
410
|
+
// levelled one, true again at the link floor.
|
|
411
|
+
if (architectural && live && refs.length === 0 && req.mayReferenceModel()) {
|
|
231
412
|
out.push({
|
|
232
413
|
severity: "error", code: ERR_REQUIREMENT_ARCH_NO_IMPLEMENTERS, name: req.name,
|
|
233
414
|
message: `architectural requirement is '${String(status)}' but nothing implements it. ` +
|
|
234
415
|
`Its check is universality — a claim set of zero means the policy is declared and unapplied.`,
|
|
235
416
|
});
|
|
236
417
|
}
|
|
418
|
+
|
|
419
|
+
// -- disposition: the decision, not the state -----------------------------
|
|
420
|
+
const disposition = req.disposition();
|
|
421
|
+
if (disposition !== undefined && !req.hasOutstandingWork()) {
|
|
422
|
+
out.push({
|
|
423
|
+
severity: "warn", code: WARN_REQUIREMENT_DISPOSITION_NOT_APPLICABLE, name: req.name,
|
|
424
|
+
message: `carries @disposition '${disposition}' but its status is '${String(status)}', which has no ` +
|
|
425
|
+
`outstanding work to decide about. A disposition is meaningful on 'planned' and 'partial' only — ` +
|
|
426
|
+
`on any other status the decision IS the status.`,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
// -- functional existence, SUBTREE-scoped ---------------------------------
|
|
430
|
+
// A functional requirement's check is EXISTENCE: it fails when nothing
|
|
431
|
+
// implements it. But an organisational tier legitimately implements nothing
|
|
432
|
+
// ITSELF — it delegates to children, and that is the whole shape of the
|
|
433
|
+
// tree. So the question is not "does this node claim anything" but "does
|
|
434
|
+
// anything in this subtree claim anything". A live L1 whose entire subtree
|
|
435
|
+
// is empty is a capability declared and built by nobody.
|
|
436
|
+
if (!architectural && live && !subtreeClaimsAnything(req)) {
|
|
437
|
+
out.push({
|
|
438
|
+
severity: "warn", code: WARN_REQUIREMENT_NOTHING_IMPLEMENTS, name: req.name,
|
|
439
|
+
message: `is '${String(status)}' but neither it nor anything nested under it names an ` +
|
|
440
|
+
`implementing node. A functional requirement's check is existence — a subtree that claims ` +
|
|
441
|
+
`nothing is a capability nobody built.`,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (disposition === REQUIREMENT_DISPOSITION_DEFERRED && req.trackedBy().length === 0) {
|
|
446
|
+
out.push({
|
|
447
|
+
severity: "warn", code: WARN_REQUIREMENT_DEFERRED_UNTRACKED, name: req.name,
|
|
448
|
+
message: `is deferred but names no @trackedBy issue. Deferring without a ticket is how a known gap ` +
|
|
449
|
+
`becomes an unknown one — nothing will raise it again.`,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
237
452
|
}
|
|
238
453
|
|
|
239
454
|
// -- object coverage: adding an entity forces a requirement -----------------
|
|
@@ -257,8 +472,7 @@ export function checkRequirements(root: MetaData): Diagnostic[] {
|
|
|
257
472
|
//
|
|
258
473
|
// So a green run means "every entity is claimed by something", not "every node is
|
|
259
474
|
// described". The stronger reading would be false.
|
|
260
|
-
for (const ent of root
|
|
261
|
-
if (ent.type !== TYPE_OBJECT || ent.subType !== OBJECT_SUBTYPE_ENTITY) continue;
|
|
475
|
+
for (const ent of coverableEntities(root)) {
|
|
262
476
|
const key = ent.resolutionKey();
|
|
263
477
|
if (!claimedObjects.has(key)) {
|
|
264
478
|
out.push({
|
|
@@ -270,3 +484,52 @@ export function checkRequirements(root: MetaData): Diagnostic[] {
|
|
|
270
484
|
|
|
271
485
|
return out;
|
|
272
486
|
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Count what the ledger contains, for the line `meta verify` prints on EVERY
|
|
490
|
+
* run — including a clean one.
|
|
491
|
+
*
|
|
492
|
+
* This exists because silence is ambiguous. A run that prints nothing cannot be
|
|
493
|
+
* told apart from a run that checked nothing, and an entry that skipped a whole
|
|
494
|
+
* grain of the model looks exactly like one that covered it. The counts below
|
|
495
|
+
* are deliberately the ones an author would otherwise have to write a script to
|
|
496
|
+
* get: how many gaps are recorded, and how many of those nobody has ruled on.
|
|
497
|
+
*/
|
|
498
|
+
export function summariseRequirements(root: MetaData): RequirementSummary | undefined {
|
|
499
|
+
const reqs = collectRequirements(root);
|
|
500
|
+
if (reqs.length === 0) return undefined; // opt-in by declaration
|
|
501
|
+
|
|
502
|
+
const summary: RequirementSummary = {
|
|
503
|
+
total: reqs.length,
|
|
504
|
+
functional: 0,
|
|
505
|
+
architectural: 0,
|
|
506
|
+
byStatus: {},
|
|
507
|
+
undecided: 0,
|
|
508
|
+
deferredUntracked: 0,
|
|
509
|
+
entitiesTotal: 0,
|
|
510
|
+
entitiesClaimed: 0,
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
for (const req of reqs) {
|
|
514
|
+
if (req.subType === REQUIREMENT_SUBTYPE_ARCHITECTURAL) summary.architectural++;
|
|
515
|
+
else summary.functional++;
|
|
516
|
+
|
|
517
|
+
const status = req.status();
|
|
518
|
+
if (status !== undefined) summary.byStatus[status] = (summary.byStatus[status] ?? 0) + 1;
|
|
519
|
+
|
|
520
|
+
if (req.hasOutstandingWork() && req.disposition() === undefined) summary.undecided++;
|
|
521
|
+
if (req.disposition() === REQUIREMENT_DISPOSITION_DEFERRED && req.trackedBy().length === 0) {
|
|
522
|
+
summary.deferredUntracked++;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Both sides of the ratio come from the SAME helpers the gate uses, so the
|
|
527
|
+
// printed summary cannot disagree with the diagnostics printed beneath it.
|
|
528
|
+
const claimed = claimedObjectKeys(root, reqs);
|
|
529
|
+
for (const ent of coverableEntities(root)) {
|
|
530
|
+
summary.entitiesTotal++;
|
|
531
|
+
if (claimed.has(ent.resolutionKey())) summary.entitiesClaimed++;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return summary;
|
|
535
|
+
}
|