@transitrix/diagrams 1.15.0 → 1.16.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/compliance/catalogue.d.ts +17 -0
- package/dist/compliance/catalogue.js +68 -0
- package/dist/compliance/index.d.ts +4 -2
- package/dist/compliance/index.js +2 -1
- package/dist/compliance/requirement-chain.d.ts +13 -2
- package/dist/compliance/requirement-chain.js +60 -33
- package/dist/goals/resolver.d.ts +1 -0
- package/dist/goals/resolver.js +44 -2
- package/dist/products/types.d.ts +1 -1
- package/dist/products/validate.js +1 -1
- package/dist/repo-validate/check-strategy-chain.js +58 -23
- package/dist/standalone/validate.js +64 -3
- package/dist/webview/render-activities.d.ts +6 -0
- package/dist/webview/render-activities.js +2 -2
- package/dist/webview/render-activity-card.d.ts +6 -0
- package/dist/webview/render-activity-card.js +2 -2
- package/dist/webview/render-blocks.d.ts +10 -0
- package/dist/webview/render-blocks.js +4 -4
- package/dist/webview/render-fgca.d.ts +6 -0
- package/dist/webview/render-fgca.js +2 -2
- package/dist/webview/render-goals.d.ts +5 -0
- package/dist/webview/render-goals.js +9 -2
- package/dist/webview/render-process-blueprint.d.ts +6 -0
- package/dist/webview/render-process-blueprint.js +2 -2
- package/dist/webview/render-products.js +1 -0
- package/dist/yaml-normalize.d.ts +3 -1
- package/dist/yaml-normalize.js +11 -3
- package/package.json +1 -1
- package/src/compliance/catalogue.ts +49 -0
- package/src/compliance/index.ts +5 -2
- package/src/compliance/requirement-chain.ts +52 -27
- package/src/goals/resolver.ts +46 -2
- package/src/products/types.ts +1 -1
- package/src/products/validate.ts +1 -1
- package/src/repo-validate/check-strategy-chain.ts +49 -23
- package/src/standalone/validate.ts +38 -2
- package/src/webview/render-activities.ts +8 -2
- package/src/webview/render-activity-card.ts +8 -3
- package/src/webview/render-blocks.ts +14 -4
- package/src/webview/render-fgca.ts +8 -2
- package/src/webview/render-goals.ts +21 -2
- package/src/webview/render-process-blueprint.ts +8 -3
- package/src/webview/render-products.ts +1 -0
- package/src/yaml-normalize.ts +12 -3
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Relative POSIX paths keep scanning policy and content identity independent of host APIs. */
|
|
2
|
+
export interface RequirementCatalogueIO {
|
|
3
|
+
read(path: string): Promise<string>;
|
|
4
|
+
list(path: string): Promise<Array<{
|
|
5
|
+
name: string;
|
|
6
|
+
kind: 'file' | 'directory' | 'symlink' | 'other';
|
|
7
|
+
}>>;
|
|
8
|
+
missing(error: unknown): boolean;
|
|
9
|
+
parse(content: string): unknown;
|
|
10
|
+
digest(content: string): string;
|
|
11
|
+
sourcePath?(path: string): string;
|
|
12
|
+
}
|
|
13
|
+
/** Shared disk/editor policy: one manifest boundary, all three zones, lossless failures. */
|
|
14
|
+
export declare function scanRequirementCatalogue(io: RequirementCatalogueIO): Promise<{
|
|
15
|
+
canon: import("./classify.js").ComplianceCanon;
|
|
16
|
+
snapshotId: string;
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { emptyCanon, ingestComplianceDoc } from './classify.js';
|
|
2
|
+
/** Shared disk/editor policy: one manifest boundary, all three zones, lossless failures. */
|
|
3
|
+
export async function scanRequirementCatalogue(io) {
|
|
4
|
+
const canon = emptyCanon();
|
|
5
|
+
const transcript = [];
|
|
6
|
+
const manifest = await io.read('transitrix.yaml');
|
|
7
|
+
const parsed = io.parse(manifest);
|
|
8
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
9
|
+
throw new Error('Invalid transitrix.yaml manifest: expected a mapping');
|
|
10
|
+
transcript.push(['transitrix.yaml', manifest]);
|
|
11
|
+
const fail = (path, message) => {
|
|
12
|
+
transcript.push(['failure', path, message]);
|
|
13
|
+
canon.findings.push({ id: `${path}:load`, owner: path, field: '', code: 'LOAD', message, reference: false, severity: 'error' });
|
|
14
|
+
};
|
|
15
|
+
const walk = async (dir, top = false) => {
|
|
16
|
+
let entries;
|
|
17
|
+
try {
|
|
18
|
+
entries = await io.list(dir);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (top && io.missing(error)) {
|
|
22
|
+
transcript.push(['absent-zone', dir]);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
fail(dir, 'Directory could not be enumerated');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (!top && entries.some(e => e.name === 'transitrix.yaml')) {
|
|
29
|
+
transcript.push(['catalogue-boundary', dir]);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
for (const { name, kind } of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
33
|
+
const path = `${dir}/${name}`;
|
|
34
|
+
if (kind === 'symlink') {
|
|
35
|
+
fail(path, 'Symbolic link was not enumerated');
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (kind === 'directory') {
|
|
39
|
+
await walk(path);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (!/\.ya?ml$/i.test(name))
|
|
43
|
+
continue;
|
|
44
|
+
if (kind !== 'file') {
|
|
45
|
+
fail(path, 'Unsupported file type');
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
let content;
|
|
49
|
+
try {
|
|
50
|
+
content = await io.read(path);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
fail(path, 'Unreadable YAML record');
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
transcript.push([path, content]);
|
|
57
|
+
try {
|
|
58
|
+
ingestComplianceDoc(canon, io.parse(content), io.sourcePath?.(path) ?? path);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
fail(path, 'Malformed YAML record');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
for (const zone of ['canon', 'codex', 'field'])
|
|
66
|
+
await walk(zone, true);
|
|
67
|
+
return { canon, snapshotId: `sha256:${io.digest(JSON.stringify(transcript))}` };
|
|
68
|
+
}
|
|
@@ -19,6 +19,8 @@ export { assertionToScoringElement, complianceConfidenceHeader, requirementToSco
|
|
|
19
19
|
export { parseCoverageMetricConfig, buildCoverageMatrix, collectCoverageViewResolutionFindings, } from './coverage-metric.js';
|
|
20
20
|
export type { CoverageMetricConfig, CoverageMetricRegimes, CoverageMetricRegimesFilter, CoverageMetricSubjects, CoverageMetricThresholds, CoverageMatrix, CoverageRow, CoverageViewFinding, ParseCoverageMetricResult, RagStatus, } from './coverage-metric.js';
|
|
21
21
|
export type { ComplianceIndex, ComplianceIndexInput, DeadlineStatus, IndexRequirement, IndexAssertion, IndexVerification, IndexNeed, IndexValidation, LawTree, LawTreeRequirement, ProductView, ProductRequirementStatus, ObjectDetailDef, ObjectDetailInput, RequirementTrace, TraceAssertionRow, TraceVerificationRow, TraceElementCatalog, TraceElementRef, TraceSourceRef, } from './types.js';
|
|
22
|
-
export { buildRequirementChain, requirementReleaseCounts, selectRequirementChain, RequirementChainSnapshot, CHAIN_STAGES, chainDate } from './requirement-chain.js';
|
|
23
|
-
export type { ChainInput, ChainScope, ChainSet, RequirementChainNode, RequirementChainEdge, RequirementChainProjection } from './requirement-chain.js';
|
|
22
|
+
export { buildSubjectRequirementChain, buildRequirementChain, requirementReleaseCounts, selectRequirementChain, RequirementChainSnapshot, CHAIN_STAGES, chainDate } from './requirement-chain.js';
|
|
23
|
+
export type { ChainSubject, ChainInput, ChainScope, ChainSet, RequirementChainNode, RequirementChainEdge, RequirementChainProjection } from './requirement-chain.js';
|
|
24
24
|
export type { ChainRecord, ChainFinding } from './types.js';
|
|
25
|
+
export { scanRequirementCatalogue } from './catalogue.js';
|
|
26
|
+
export type { RequirementCatalogueIO } from './catalogue.js';
|
package/dist/compliance/index.js
CHANGED
|
@@ -11,4 +11,5 @@ export { renderComplianceHtml, renderImpactMatrixHtml } from './html.js';
|
|
|
11
11
|
export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, collectImpactViewResolutionFindings, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, extractProcessFlowTasks, mergeStageTaskDetails, computeDeadlineStatus } from './impact.js';
|
|
12
12
|
export { assertionToScoringElement, complianceConfidenceHeader, requirementToScoringElement, scoreComplianceView, } from './confidence.js';
|
|
13
13
|
export { parseCoverageMetricConfig, buildCoverageMatrix, collectCoverageViewResolutionFindings, } from './coverage-metric.js';
|
|
14
|
-
export { buildRequirementChain, requirementReleaseCounts, selectRequirementChain, RequirementChainSnapshot, CHAIN_STAGES, chainDate } from './requirement-chain.js';
|
|
14
|
+
export { buildSubjectRequirementChain, buildRequirementChain, requirementReleaseCounts, selectRequirementChain, RequirementChainSnapshot, CHAIN_STAGES, chainDate } from './requirement-chain.js';
|
|
15
|
+
export { scanRequirementCatalogue } from './catalogue.js';
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { ChainRecord, ChainFinding, ComplianceIndex } from './types.js';
|
|
2
2
|
export declare const CHAIN_STAGES: readonly ["Source document", "Driver", "Need", "Stakeholder requirement", "System requirement", "Software requirement", "Unclassified requirement", "Verification definition", "Result"];
|
|
3
|
+
export interface ChainSubject {
|
|
4
|
+
type: 'PRODUCT' | 'APPLICATION';
|
|
5
|
+
id: string;
|
|
6
|
+
}
|
|
3
7
|
export interface ChainScope {
|
|
4
8
|
catalogue: string;
|
|
9
|
+
subject?: ChainSubject;
|
|
5
10
|
product?: string;
|
|
6
11
|
release?: string;
|
|
7
12
|
project?: string;
|
|
@@ -46,7 +51,7 @@ export interface ChainAssignment {
|
|
|
46
51
|
relations: string[];
|
|
47
52
|
}
|
|
48
53
|
export interface RequirementChainProjection {
|
|
49
|
-
contract: 'requirement-chain/0.2';
|
|
54
|
+
contract: 'requirement-chain/0.2' | 'requirement-chain/0.3';
|
|
50
55
|
contractRevision: string;
|
|
51
56
|
asAt: string;
|
|
52
57
|
snapshotId: string;
|
|
@@ -89,8 +94,14 @@ export interface RequirementChainProjection {
|
|
|
89
94
|
executions: Record<string, string[]>;
|
|
90
95
|
}
|
|
91
96
|
export declare function chainDate(v: unknown): v is string;
|
|
92
|
-
/**
|
|
97
|
+
/** Explicit PRODUCT compatibility adapter for requirement-chain/0.2. */
|
|
93
98
|
export declare function buildRequirementChain(input: ChainInput): RequirementChainProjection;
|
|
99
|
+
/** Subject-aware 0.3 entry point; both versions use the same deterministic projection. */
|
|
100
|
+
export declare function buildSubjectRequirementChain(input: ChainInput & {
|
|
101
|
+
scope: ChainScope & {
|
|
102
|
+
subject: ChainSubject;
|
|
103
|
+
};
|
|
104
|
+
}): RequirementChainProjection;
|
|
94
105
|
/** Both is two independent walks, never an expansion from the union frontier. */
|
|
95
106
|
export declare function selectRequirementChain(p: RequirementChainProjection, focus?: string, direction?: 'upstream' | 'downstream' | 'both'): {
|
|
96
107
|
nodes: RequirementChainNode[];
|
|
@@ -26,10 +26,28 @@ function freeze(value) {
|
|
|
26
26
|
}
|
|
27
27
|
const CODEX = ['LAW', 'REGULATION', 'STANDARD', 'POLICY', 'INTERNAL_STANDARD', 'PRINCIPLE'];
|
|
28
28
|
const FIELD = ['INTERVIEW', 'SURVEY', 'OBSERVATION', 'DRAFT'];
|
|
29
|
-
/**
|
|
29
|
+
/** Explicit PRODUCT compatibility adapter for requirement-chain/0.2. */
|
|
30
30
|
export function buildRequirementChain(input) {
|
|
31
|
+
if (input.scope.subject)
|
|
32
|
+
throw new Error('Use buildSubjectRequirementChain for explicit subjects');
|
|
33
|
+
return projectRequirementChain(input);
|
|
34
|
+
}
|
|
35
|
+
/** Subject-aware 0.3 entry point; both versions use the same deterministic projection. */
|
|
36
|
+
export function buildSubjectRequirementChain(input) {
|
|
37
|
+
if (input.scope.product !== undefined)
|
|
38
|
+
throw new Error('Conflicting product and subject selectors');
|
|
39
|
+
if (!['PRODUCT', 'APPLICATION'].includes(input.scope.subject.type))
|
|
40
|
+
throw new Error('Unsupported subject type');
|
|
41
|
+
return projectRequirementChain(input);
|
|
42
|
+
}
|
|
43
|
+
/** Pure, lossless release projection; no legacy coverage verdict is reinterpreted. */
|
|
44
|
+
function projectRequirementChain(input) {
|
|
31
45
|
var _a;
|
|
32
46
|
const { index, asAt, scope } = input;
|
|
47
|
+
const subject = scope.subject ?? { type: 'PRODUCT', id: scope.product };
|
|
48
|
+
const membershipKind = subject.type === 'APPLICATION' ? 'application_scope' : 'product_scope';
|
|
49
|
+
const pairKind = subject.type === 'APPLICATION' ? 'project_application' : 'project_product';
|
|
50
|
+
const subjectWord = scope.subject ? 'subject' : 'product';
|
|
33
51
|
const findings = structuredClone(index.findings);
|
|
34
52
|
const scopeFindings = [];
|
|
35
53
|
const records = new Map();
|
|
@@ -39,7 +57,7 @@ export function buildRequirementChain(input) {
|
|
|
39
57
|
if (!findings.some(f => f.id === id && f.code === code))
|
|
40
58
|
findings.push({ id, owner, field, code, message, reference, severity: warning ? 'warning' : 'error' });
|
|
41
59
|
};
|
|
42
|
-
for (const [id, versions] of index.recordsById) {
|
|
60
|
+
for (const [id, versions] of [...index.recordsById].sort(([a], [b]) => a.localeCompare(b))) {
|
|
43
61
|
if (versions.length !== 1) {
|
|
44
62
|
finding(id, 'id', 'DUPLICATE', 'Ambiguous catalogue identity');
|
|
45
63
|
complete = false;
|
|
@@ -109,7 +127,7 @@ export function buildRequirementChain(input) {
|
|
|
109
127
|
node(r, 2);
|
|
110
128
|
else if (r.type === 'DRIVER')
|
|
111
129
|
node(r, 1);
|
|
112
|
-
else if (['PRODUCT', 'RELEASE', 'ACTION'].includes(r.type))
|
|
130
|
+
else if (['PRODUCT', 'APPLICATION', 'RELEASE', 'ACTION'].includes(r.type))
|
|
113
131
|
node(r, -1);
|
|
114
132
|
else if (CODEX.includes(r.type) || FIELD.includes(r.type)) {
|
|
115
133
|
node(r, 0);
|
|
@@ -165,13 +183,15 @@ export function buildRequirementChain(input) {
|
|
|
165
183
|
const src = records.get(from), dst = records.get(to);
|
|
166
184
|
const endpoints = kind === 'requirement_parent' ? is(from, 'REQUIREMENT') && is(to, 'REQUIREMENT') :
|
|
167
185
|
kind === 'serves' ? is(from, 'REQUIREMENT') && is(to, 'NEED') :
|
|
168
|
-
kind === '
|
|
169
|
-
kind === '
|
|
170
|
-
kind === '
|
|
171
|
-
kind === '
|
|
172
|
-
kind === '
|
|
173
|
-
|
|
174
|
-
|
|
186
|
+
kind === 'application_scope' ? is(from, 'REQUIREMENT') && is(to, 'APPLICATION') :
|
|
187
|
+
kind === 'project_application' ? isProject(from) && is(to, 'APPLICATION') :
|
|
188
|
+
kind === 'product_scope' ? is(from, 'REQUIREMENT') && is(to, 'PRODUCT') :
|
|
189
|
+
kind === 'project_scope' ? is(from, 'REQUIREMENT') && isProject(to) :
|
|
190
|
+
kind === 'project_product' ? isProject(from) && is(to, 'PRODUCT') :
|
|
191
|
+
kind === 'required_for' ? is(from, 'REQUIREMENT') && is(to, 'RELEASE') :
|
|
192
|
+
kind === 'source_trace' ? !!src && !!dst && envelope.get(from) === true && envelope.get(to) === true &&
|
|
193
|
+
(['REQUIREMENT', 'NEED'].includes(src.type) && (dst.type === 'DRIVER' || FIELD.includes(dst.type)) ||
|
|
194
|
+
src.type === 'DRIVER' && FIELD.includes(dst.type)) : !!src && !!dst;
|
|
175
195
|
const temporal = contains(r, src) && contains(r, dst);
|
|
176
196
|
if (!temporal)
|
|
177
197
|
finding(r.id, 'to', 'REL-003', 'Relation window must be contained in both endpoints', true);
|
|
@@ -207,38 +227,38 @@ export function buildRequirementChain(input) {
|
|
|
207
227
|
}
|
|
208
228
|
}
|
|
209
229
|
const memberships = (id, kind) => rels.filter(r => usable.has(r.id) && r.raw.type === kind && r.raw.from === id).map(r => String(r.raw.to));
|
|
210
|
-
const unresolved = requirements.filter(r => active.get(r.id) && memberships(r.id, 'product_scope').length === 0).map(r => r.id);
|
|
211
|
-
const P = requirements.filter(r => active.get(r.id) && memberships(r.id,
|
|
230
|
+
const unresolved = requirements.filter(r => active.get(r.id) && memberships(r.id, 'product_scope').length === 0 && (!scope.subject || memberships(r.id, 'application_scope').length === 0)).map(r => r.id);
|
|
231
|
+
const P = requirements.filter(r => active.get(r.id) && memberships(r.id, membershipKind).includes(subject.id ?? '')).map(r => r.id);
|
|
212
232
|
let productComplete = complete && unresolved.length === 0;
|
|
213
233
|
if (unresolved.length)
|
|
214
|
-
scopeFindings.push(
|
|
215
|
-
if (requirements.some(r => !envelope.get(r.id)) || rels.some(r => r.raw.type ===
|
|
234
|
+
scopeFindings.push(`Unresolved ${subjectWord} membership`);
|
|
235
|
+
if (requirements.some(r => !envelope.get(r.id)) || rels.some(r => r.raw.type === membershipKind && active.get(r.id) && !usable.has(r.id)))
|
|
216
236
|
productComplete = false;
|
|
217
237
|
const invalidAssignments = new Set();
|
|
218
238
|
for (const r of rels.filter(r => r.raw.type === 'required_for')) {
|
|
219
239
|
const from = String(r.raw.from), to = records.get(String(r.raw.to));
|
|
220
240
|
const historical = chainDate(r.raw.valid_to) && r.raw.valid_to < asAt;
|
|
221
241
|
const future = chainDate(r.raw.valid_from) && r.raw.valid_from > asAt;
|
|
222
|
-
if (!historical && !future && (!usable.has(r.id) || !memberships(from, 'product_scope').includes(String(to?.raw.of)))) {
|
|
242
|
+
if (!historical && !future && (!usable.has(r.id) || ![...memberships(from, 'product_scope'), ...(scope.subject ? memberships(from, 'application_scope') : [])].includes(String(to?.raw.of)))) {
|
|
223
243
|
invalidAssignments.add(from);
|
|
224
|
-
finding(r.id, 'to', 'ASSIGNMENT',
|
|
244
|
+
finding(r.id, 'to', 'ASSIGNMENT', `Invalid, dangling or wrong-${subjectWord} assignment`, true);
|
|
225
245
|
if (!envelope.get(r.id))
|
|
226
246
|
productComplete = false;
|
|
227
247
|
}
|
|
228
248
|
}
|
|
229
249
|
let selectedComplete = complete;
|
|
230
|
-
if (!is(
|
|
250
|
+
if (!is(subject.id, subject.type)) {
|
|
231
251
|
productComplete = false;
|
|
232
252
|
selectedComplete = false;
|
|
233
|
-
scopeFindings.push('Product unselected or unresolved');
|
|
253
|
+
scopeFindings.push(scope.subject ? 'Subject unselected or unresolved' : 'Product unselected or unresolved');
|
|
234
254
|
}
|
|
235
|
-
if (!is(scope.release, 'RELEASE') || records.get(scope.release ?? '')?.raw.of !==
|
|
255
|
+
if (!is(scope.release, 'RELEASE') || records.get(scope.release ?? '')?.raw.of !== subject.id) {
|
|
236
256
|
selectedComplete = false;
|
|
237
|
-
scopeFindings.push(
|
|
257
|
+
scopeFindings.push(`Release unselected, unresolved or wrong ${subjectWord}`);
|
|
238
258
|
}
|
|
239
|
-
if (scope.project && (!isProject(scope.project) || !memberships(scope.project,
|
|
259
|
+
if (scope.project && (!isProject(scope.project) || !memberships(scope.project, pairKind).includes(subject.id ?? ''))) {
|
|
240
260
|
selectedComplete = false;
|
|
241
|
-
scopeFindings.push(
|
|
261
|
+
scopeFindings.push(`Invalid or unmodelled project/${subjectWord} pair`);
|
|
242
262
|
}
|
|
243
263
|
function query(release) {
|
|
244
264
|
const chain = [];
|
|
@@ -246,9 +266,9 @@ export function buildRequirementChain(input) {
|
|
|
246
266
|
let valid = true;
|
|
247
267
|
while (cursor) {
|
|
248
268
|
const r = records.get(cursor);
|
|
249
|
-
if (chain.includes(cursor) || !is(cursor, 'RELEASE') || r?.raw.of !==
|
|
269
|
+
if (chain.includes(cursor) || !is(cursor, 'RELEASE') || r?.raw.of !== subject.id) {
|
|
250
270
|
valid = false;
|
|
251
|
-
finding(chain[chain.length - 1] ?? release, 'predecessor', 'RELEASE-SCOPE', chain.includes(cursor) ? 'Release predecessor cycle' :
|
|
271
|
+
finding(chain[chain.length - 1] ?? release, 'predecessor', 'RELEASE-SCOPE', chain.includes(cursor) ? 'Release predecessor cycle' : `Missing, invalid or cross-${subjectWord} predecessor`);
|
|
252
272
|
break;
|
|
253
273
|
}
|
|
254
274
|
chain.push(cursor);
|
|
@@ -275,16 +295,16 @@ export function buildRequirementChain(input) {
|
|
|
275
295
|
selectedComplete = false;
|
|
276
296
|
scopeFindings.push('Unresolved project membership');
|
|
277
297
|
}
|
|
278
|
-
if (rels.some(r => [
|
|
298
|
+
if (rels.some(r => [membershipKind, 'project_scope', 'required_for'].includes(String(r.raw.type)) && !envelope.get(r.id)))
|
|
279
299
|
selectedComplete = false;
|
|
280
|
-
if (scope.project && L.some(id => memberships(id, 'project_scope').some(project => !memberships(project,
|
|
300
|
+
if (scope.project && L.some(id => memberships(id, 'project_scope').some(project => !memberships(project, pairKind).includes(subject.id ?? '')))) {
|
|
281
301
|
selectedComplete = false;
|
|
282
|
-
scopeFindings.push(
|
|
302
|
+
scopeFindings.push(`Inconsistent project/${subjectWord} membership`);
|
|
283
303
|
}
|
|
284
304
|
const S = L.filter(id => !scope.project || memberships(id, 'project_scope').includes(scope.project));
|
|
285
305
|
const elsewhere = new Set();
|
|
286
306
|
for (const release of records.values())
|
|
287
|
-
if (release.type === 'RELEASE' && release.raw.of ===
|
|
307
|
+
if (release.type === 'RELEASE' && release.raw.of === subject.id) {
|
|
288
308
|
const q = query(release.id);
|
|
289
309
|
productComplete && (productComplete = q.complete);
|
|
290
310
|
q.ids.forEach(id => elsewhere.add(id));
|
|
@@ -352,7 +372,7 @@ export function buildRequirementChain(input) {
|
|
|
352
372
|
if (v.raw.verifies === r.id)
|
|
353
373
|
owners.add(v.id);
|
|
354
374
|
for (const rel of rels) {
|
|
355
|
-
const ownScope = [
|
|
375
|
+
const ownScope = [membershipKind, 'project_scope', 'required_for'].includes(String(rel.raw.type));
|
|
356
376
|
if (ownScope ? rel.raw.from === r.id : owners.has(String(rel.raw.from)))
|
|
357
377
|
owners.add(rel.id);
|
|
358
378
|
}
|
|
@@ -379,15 +399,17 @@ export function buildRequirementChain(input) {
|
|
|
379
399
|
const broken = S.filter(id => selectedRefs.some(f => affectedRequirements[f.id]?.includes(id)));
|
|
380
400
|
for (const n of nodes)
|
|
381
401
|
if (records.get(n.recordId)?.type === 'REQUIREMENT' && !S.includes(n.recordId)) {
|
|
382
|
-
n.context.push(!active.get(n.recordId) ? 'inactive' : !P.includes(n.recordId) ?
|
|
402
|
+
n.context.push(!active.get(n.recordId) ? 'inactive' : !P.includes(n.recordId) ? `unresolved or other ${subjectWord}` :
|
|
383
403
|
!L.includes(n.recordId) ? 'other release or unassigned' : 'other project');
|
|
384
404
|
}
|
|
405
|
+
if (scope.subject && (!is(subject.id, subject.type) || !is(scope.release, 'RELEASE') || records.get(scope.release ?? '')?.raw.of !== subject.id))
|
|
406
|
+
productComplete = false;
|
|
385
407
|
const unassignedSet = set(unassigned, productComplete);
|
|
386
408
|
const result = {
|
|
387
|
-
contract: 'requirement-chain/0.2', contractRevision: '97c9d41819011ead8fe192c266cba32707eae82f', asAt, snapshotId: input.snapshotId, sourceRevision: input.sourceRevision,
|
|
409
|
+
contract: scope.subject ? 'requirement-chain/0.3' : 'requirement-chain/0.2', contractRevision: scope.subject ? 'a131b1862d86f34be8feb729e1cc6ebc994c3891' : '97c9d41819011ead8fe192c266cba32707eae82f', asAt, snapshotId: input.snapshotId, sourceRevision: input.sourceRevision,
|
|
388
410
|
scope: structuredClone(scope), completeness: productComplete && selectedComplete ? 'complete' : 'incomplete', scopeFindings: sorted(scopeFindings),
|
|
389
411
|
populations: { product: set(P, productComplete), release: set(L, selectedComplete), selected: set(S, selectedComplete),
|
|
390
|
-
inactive: set(requirements.filter(r => envelope.get(r.id) && !active.get(r.id) && rels.some(rel => rel.raw.type ===
|
|
412
|
+
inactive: set(requirements.filter(r => envelope.get(r.id) && !active.get(r.id) && rels.some(rel => rel.raw.type === membershipKind && rel.raw.from === r.id && rel.raw.to === subject.id && envelope.get(rel.id))).map(r => r.id), complete),
|
|
391
413
|
unresolved: set(unresolved, complete) },
|
|
392
414
|
assignments: { here: set(L, selectedComplete), otherReleaseOnly: set(P.filter(id => elsewhere.has(id) && !L.includes(id)), productComplete),
|
|
393
415
|
unassigned: unassignedSet, invalid: set(P.filter(id => invalidAssignments.has(id)), complete), provenance: selected.provenance.sort((a, b) => a.requirement.localeCompare(b.requirement)) },
|
|
@@ -474,6 +496,9 @@ export function requirementReleaseCounts(p) {
|
|
|
474
496
|
const contextUnits = { 0: 'source documents', 1: 'drivers', 2: 'needs', 7: 'definition parts', 8: 'result parts' };
|
|
475
497
|
for (const stage of [0, 1, 2, 7, 8])
|
|
476
498
|
add('context-' + stage, CHAIN_STAGES[stage], set(view.nodes.filter(n => n.stage === stage).map(n => n.id), p.populations.selected.completeness === 'complete'), contextUnits[stage]);
|
|
499
|
+
if (p.scope.subject)
|
|
500
|
+
for (const count of Object.values(counts))
|
|
501
|
+
count.label = count.label.replace(/whole product/g, 'whole subject').replace('Product requirements', 'Subject requirements');
|
|
477
502
|
return freeze(counts);
|
|
478
503
|
}
|
|
479
504
|
/** Monotonic refresh gate shared by reports; late successes and failures cannot replace newer data. */
|
|
@@ -488,7 +513,9 @@ export class RequirementChainSnapshot {
|
|
|
488
513
|
const input = await load();
|
|
489
514
|
if (generation !== this.generation)
|
|
490
515
|
return false;
|
|
491
|
-
this.current =
|
|
516
|
+
this.current = input.scope.subject
|
|
517
|
+
? buildSubjectRequirementChain({ ...input, scope: { ...input.scope, subject: input.scope.subject } })
|
|
518
|
+
: buildRequirementChain(input);
|
|
492
519
|
this.stale = false;
|
|
493
520
|
return true;
|
|
494
521
|
}
|
package/dist/goals/resolver.d.ts
CHANGED
package/dist/goals/resolver.js
CHANGED
|
@@ -42,10 +42,51 @@ function collectGoalElements(docs) {
|
|
|
42
42
|
if (!id)
|
|
43
43
|
continue;
|
|
44
44
|
if (!out.has(id))
|
|
45
|
-
out.set(id, doc); //
|
|
45
|
+
out.set(id, { ...doc }); // never mutate the caller's catalogue
|
|
46
46
|
}
|
|
47
47
|
return out;
|
|
48
48
|
}
|
|
49
|
+
/** Materialize canonical child -> parent links for a single-parent tree.
|
|
50
|
+
* Never mutate canon or silently linearize an ambiguous hierarchy.
|
|
51
|
+
*/
|
|
52
|
+
function applyGoalParentRelations(goals, relations, validAt) {
|
|
53
|
+
const declaredChildren = new Set();
|
|
54
|
+
const parents = new Map();
|
|
55
|
+
for (const doc of relations) {
|
|
56
|
+
if (!isObject(doc) || doc['notation'] !== 'relation' || doc['type'] !== 'goal_parent')
|
|
57
|
+
continue;
|
|
58
|
+
const child = str(doc['from']);
|
|
59
|
+
const parent = str(doc['to']);
|
|
60
|
+
if (!child || !parent || !goals.has(child) || !goals.has(parent)) {
|
|
61
|
+
throw new Error(`goal_parent relation ${str(doc['id']) ?? '(unnamed)'} must reference two existing goals`);
|
|
62
|
+
}
|
|
63
|
+
declaredChildren.add(child);
|
|
64
|
+
const from = str(doc['valid_from']);
|
|
65
|
+
const to = str(doc['valid_to']);
|
|
66
|
+
if (validAt && ((from && validAt < from) || (to && validAt > to)))
|
|
67
|
+
continue;
|
|
68
|
+
const choices = parents.get(child) ?? new Set();
|
|
69
|
+
choices.add(parent);
|
|
70
|
+
parents.set(child, choices);
|
|
71
|
+
}
|
|
72
|
+
for (const child of declaredChildren) {
|
|
73
|
+
const goal = goals.get(child);
|
|
74
|
+
const choices = parents.get(child) ?? new Set();
|
|
75
|
+
if (choices.size > 1) {
|
|
76
|
+
throw new Error(`goal_parent for ${child} has multiple parents in this scope; narrow valid_at or use a graph view`);
|
|
77
|
+
}
|
|
78
|
+
const parent = [...choices][0];
|
|
79
|
+
const legacyParent = str(goal['parent']);
|
|
80
|
+
if (parent && legacyParent && parent !== legacyParent) {
|
|
81
|
+
throw new Error(`goal_parent for ${child} conflicts with its inline parent`);
|
|
82
|
+
}
|
|
83
|
+
// Declared but inactive links must not fall back to an undated parent.
|
|
84
|
+
if (parent)
|
|
85
|
+
goal['parent'] = parent;
|
|
86
|
+
else
|
|
87
|
+
delete goal['parent'];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
49
90
|
/**
|
|
50
91
|
* Returns true when the parsed view document uses the canon-projection form
|
|
51
92
|
* (`view_config` key present, no inline `goals[]`). Used by the preview and
|
|
@@ -69,6 +110,8 @@ export function resolveGoals(viewDoc, sources) {
|
|
|
69
110
|
const vc = isObject(viewDoc['view_config']) ? viewDoc['view_config'] : {};
|
|
70
111
|
const scope = isObject(vc['scope']) ? vc['scope'] : {};
|
|
71
112
|
const allGoals = collectGoalElements(sources.elements);
|
|
113
|
+
const validAt = str(scope['valid_at']);
|
|
114
|
+
applyGoalParentRelations(allGoals, sources.relations ?? [], validAt);
|
|
72
115
|
// 1. Scope by root_goal (this goal + its descendants), or every goal when
|
|
73
116
|
// omitted (§5.1: "Omit to show all root goals").
|
|
74
117
|
const rootGoal = str(scope['root_goal']);
|
|
@@ -97,7 +140,6 @@ export function resolveGoals(viewDoc, sources) {
|
|
|
97
140
|
// 4. Narrow by valid_at — only goals in effect on that date. Goals with no
|
|
98
141
|
// valid_from are not lifecycle-tracked and are kept (permissive default,
|
|
99
142
|
// matching the action/dgca resolvers' stance).
|
|
100
|
-
const validAt = str(scope['valid_at']);
|
|
101
143
|
if (validAt) {
|
|
102
144
|
candidateIds = new Set([...candidateIds].filter((id) => {
|
|
103
145
|
const el = allGoals.get(id);
|
package/dist/products/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ProductType = 'digital_product' | 'service' | 'platform' | 'bundle';
|
|
1
|
+
export type ProductType = 'physical_product' | 'digital_product' | 'service' | 'platform' | 'bundle';
|
|
2
2
|
export type ProductStatus = 'Draft' | 'Active' | 'Deprecated';
|
|
3
3
|
export interface Product {
|
|
4
4
|
product_id: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { schemaFinding } from '../schema-finding.js';
|
|
2
|
-
const VALID_TYPES = new Set(['digital_product', 'service', 'platform', 'bundle']);
|
|
2
|
+
const VALID_TYPES = new Set(['physical_product', 'digital_product', 'service', 'platform', 'bundle']);
|
|
3
3
|
const VALID_STATUSES = new Set(['Draft', 'Active', 'Deprecated']);
|
|
4
4
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
5
5
|
export function validateProductsCatalogue(input) {
|
|
@@ -17,16 +17,11 @@ import { validateActionFields } from '../activities/action-fields.js';
|
|
|
17
17
|
// classification (`Issue.Severity` in the Go source) and its "import
|
|
18
18
|
// anyway, record the warning" policy.
|
|
19
19
|
//
|
|
20
|
-
// GOALS-
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// this shape and do surface GOALS-011 warnings — that noise is the accepted
|
|
26
|
-
// cost of a *warning* level rule (non-blocking, advisory), unlike the ERROR
|
|
27
|
-
// tier this repo held the line on. This is a deliberate call, not an
|
|
28
|
-
// oversight: keep the coverage rather than drop it, now that it can be
|
|
29
|
-
// non-blocking.
|
|
20
|
+
// GOALS-010 checks canonical and legacy edges in overlapping lifecycle slices.
|
|
21
|
+
// GOALS-009/011 respect the canonical goal_parent records already loaded
|
|
22
|
+
// from canon/relations. Static catalogue coverage considers those links;
|
|
23
|
+
// date-scoped hierarchy selection and ambiguity are checked by Goals views.
|
|
24
|
+
// Legacy inline parents remain supported for transitional catalogues.
|
|
30
25
|
//
|
|
31
26
|
// GOALS-008 (type/level mismatch) is still NOT ported, at either severity.
|
|
32
27
|
// Both of DSM's cases ("type not declared in goal_types" / "level doesn't
|
|
@@ -107,17 +102,15 @@ function collectByNotation(elements, matches) {
|
|
|
107
102
|
}
|
|
108
103
|
return out;
|
|
109
104
|
}
|
|
110
|
-
/** DFS cycle detection over
|
|
105
|
+
/** DFS cycle detection over resolved parent edges, matching DSM's
|
|
111
106
|
* `findCycle` (goals.go): only walks an edge to a parent that itself resolves
|
|
112
107
|
* within the set — an edge into an unresolved (orphan) parent is not walked,
|
|
113
108
|
* so an orphan can never be mistaken for a cycle. Returns the id where a
|
|
114
109
|
* back-edge was hit, or `undefined` if the graph is acyclic. */
|
|
115
|
-
function findParentCycle(goals) {
|
|
116
|
-
const parentOf = new Map();
|
|
110
|
+
function findParentCycle(goals, parents) {
|
|
117
111
|
const known = new Set();
|
|
118
112
|
for (const g of goals) {
|
|
119
113
|
known.add(g.id);
|
|
120
|
-
parentOf.set(g.id, readString(g.data, 'parent'));
|
|
121
114
|
}
|
|
122
115
|
const UNVISITED = 0;
|
|
123
116
|
const VISITING = 1;
|
|
@@ -130,8 +123,9 @@ function findParentCycle(goals) {
|
|
|
130
123
|
if (s === VISITING)
|
|
131
124
|
return id;
|
|
132
125
|
state.set(id, VISITING);
|
|
133
|
-
const parent
|
|
134
|
-
|
|
126
|
+
for (const parent of parents.get(id) ?? []) {
|
|
127
|
+
if (!known.has(parent))
|
|
128
|
+
continue;
|
|
135
129
|
const hit = walk(parent);
|
|
136
130
|
if (hit)
|
|
137
131
|
return hit;
|
|
@@ -184,27 +178,68 @@ function findPredecessorCycle(actions) {
|
|
|
184
178
|
}
|
|
185
179
|
return undefined;
|
|
186
180
|
}
|
|
187
|
-
/** GOALS-010 —
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
181
|
+
/** GOALS-010 — parent links must be acyclic in every effective date slice.
|
|
182
|
+
* A cycle can first appear only when an edge starts. Inspect those boundaries
|
|
183
|
+
* and the unbounded past; do not union disjoint historical parent relations.
|
|
184
|
+
*/
|
|
185
|
+
function checkGoalParentCycle(goals, relations, findings) {
|
|
186
|
+
const known = new Set(goals.map(g => g.id));
|
|
187
|
+
const links = relations.flatMap(rel => {
|
|
188
|
+
const d = rel.data;
|
|
189
|
+
if (!d || d['notation'] !== 'relation' || d['type'] !== 'goal_parent')
|
|
190
|
+
return [];
|
|
191
|
+
const child = readString(d, 'from'), parent = readString(d, 'to');
|
|
192
|
+
if (!child || !parent || !known.has(child) || !known.has(parent))
|
|
193
|
+
return [];
|
|
194
|
+
return [{ child, parent, from: readString(d, 'valid_from'), to: readString(d, 'valid_to') }];
|
|
195
|
+
});
|
|
196
|
+
const declared = new Set(links.map(link => link.child));
|
|
197
|
+
const dates = [...new Set(['0000-01-01', ...links.flatMap(link => link.from ? [link.from] : [])])].sort();
|
|
198
|
+
for (const date of dates) {
|
|
199
|
+
const parents = new Map();
|
|
200
|
+
for (const g of goals) {
|
|
201
|
+
const legacy = readString(g.data, 'parent');
|
|
202
|
+
if (!declared.has(g.id) && legacy)
|
|
203
|
+
parents.set(g.id, [legacy]);
|
|
204
|
+
}
|
|
205
|
+
for (const link of links) {
|
|
206
|
+
if ((link.from && date < link.from) || (link.to && date > link.to))
|
|
207
|
+
continue;
|
|
208
|
+
parents.set(link.child, [...(parents.get(link.child) ?? []), link.parent]);
|
|
209
|
+
}
|
|
210
|
+
const cyc = findParentCycle(goals, parents);
|
|
211
|
+
if (!cyc)
|
|
212
|
+
continue;
|
|
191
213
|
findings.push({
|
|
192
214
|
scope: PScope,
|
|
193
215
|
id: cyc,
|
|
194
216
|
ruleId: 'GOALS-010',
|
|
195
217
|
message: `GOALS-010: parent chain contains a cycle involving goal '${cyc}'.`,
|
|
196
218
|
});
|
|
219
|
+
return;
|
|
197
220
|
}
|
|
198
221
|
}
|
|
199
222
|
/** GOALS-009 / GOALS-011 — GOAL `parent` resolution (warning). Mirrors DSM's
|
|
200
223
|
* goals.go: the two are mutually exclusive per goal — a goal with no
|
|
201
224
|
* `parent` is checked for GOALS-011 (backlog, level >= 1); a goal with a
|
|
202
225
|
* `parent` is checked for GOALS-009 (orphan, parent unresolved) instead. */
|
|
203
|
-
function checkGoalParentResolution(goals, findings) {
|
|
226
|
+
function checkGoalParentResolution(goals, relations, findings) {
|
|
204
227
|
const knownIds = new Set(goals.map((g) => g.id));
|
|
228
|
+
const linkedChildren = new Set();
|
|
229
|
+
for (const rel of relations) {
|
|
230
|
+
const data = rel.data;
|
|
231
|
+
if (!data || data['notation'] !== 'relation' || data['type'] !== 'goal_parent')
|
|
232
|
+
continue;
|
|
233
|
+
const child = readString(data, 'from');
|
|
234
|
+
const parent = readString(data, 'to');
|
|
235
|
+
if (child && parent && knownIds.has(child) && knownIds.has(parent))
|
|
236
|
+
linkedChildren.add(child);
|
|
237
|
+
}
|
|
205
238
|
for (const g of goals) {
|
|
206
239
|
const parent = readString(g.data, 'parent');
|
|
207
240
|
if (!parent) {
|
|
241
|
+
if (linkedChildren.has(g.id))
|
|
242
|
+
continue;
|
|
208
243
|
const level = readFiniteNumber(g.data, 'level');
|
|
209
244
|
if (level !== undefined && level >= 1) {
|
|
210
245
|
findings.push({
|
|
@@ -467,8 +502,8 @@ export function checkStrategyChainSemantics(input, findings) {
|
|
|
467
502
|
const actions = collectByNotation(input.elements, isActionNotation);
|
|
468
503
|
const drivers = collectByNotation(input.elements, isDriverNotation);
|
|
469
504
|
const changes = collectByNotation(input.elements, isChangeNotation);
|
|
470
|
-
checkGoalParentCycle(goals, findings);
|
|
471
|
-
checkGoalParentResolution(goals, findings);
|
|
505
|
+
checkGoalParentCycle(goals, input.relations, findings);
|
|
506
|
+
checkGoalParentResolution(goals, input.relations, findings);
|
|
472
507
|
checkActionSelfPredecessor(actions, findings);
|
|
473
508
|
checkActionPredecessorCycle(actions, findings);
|
|
474
509
|
checkActionOrphanReferences(actions, findings);
|