@principal-ai/subsystems-core 0.49.2 → 0.51.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.
- package/dist/node.d.ts +1 -2
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +1 -3
- package/dist/node.js.map +1 -1
- package/dist/storage/topic-types.d.ts +14 -132
- package/dist/storage/topic-types.d.ts.map +1 -1
- package/dist/storage/topic-types.js +3 -47
- package/dist/storage/topic-types.js.map +1 -1
- package/dist/storage/topicStore.d.ts +2 -4
- package/dist/storage/topicStore.d.ts.map +1 -1
- package/dist/storage/topicStore.js +0 -1
- package/dist/storage/topicStore.js.map +1 -1
- package/dist/types/subsystem-model.d.ts +45 -21
- package/dist/types/subsystem-model.d.ts.map +1 -1
- package/dist/types/subsystem-model.js +1 -2
- package/dist/types/subsystem-model.js.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +43 -11
- package/dist/validation.js.map +1 -1
- package/package.json +1 -1
- package/schemas/subsystem-model.schema.json +69 -13
- package/src/node.ts +0 -4
- package/src/storage/topic-types.ts +14 -156
- package/src/storage/topicStore.test.ts +4 -21
- package/src/storage/topicStore.ts +2 -5
- package/src/types/subsystem-model.test.ts +5 -0
- package/src/types/subsystem-model.ts +47 -21
- package/src/validation.test.ts +65 -5
- package/src/validation.ts +57 -13
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Ontology: construct = what a node is, framework + stereotype = which
|
|
15
15
|
* framework pattern it plays, role = where it sits, process = where it runs,
|
|
16
|
-
* module = which source file/module the export belongs to
|
|
17
|
-
* proposed = not yet in source (design / migration placeholder).
|
|
16
|
+
* module = which source file/module the export belongs to.
|
|
18
17
|
* Package (repo) frames are derived from component `purl` when a graph spans
|
|
19
18
|
* multiple repos — there is no separate `package` field.
|
|
20
19
|
* `symbol` is the code identity; `name` is the display label.
|
|
@@ -325,19 +324,10 @@ export interface SubsystemComponent {
|
|
|
325
324
|
logo?: string;
|
|
326
325
|
purpose?: string;
|
|
327
326
|
role?: SubsystemComponentRole;
|
|
328
|
-
/**
|
|
329
|
-
* Design / migration placeholder — participates in edges and flows but is
|
|
330
|
-
* not a live source declaration yet. Verification skips source checks until
|
|
331
|
-
* promoted (`proposed` cleared, `file` + `symbol` filled). Orthogonal to
|
|
332
|
-
* `construct` (intended shape) and `role` (topology).
|
|
333
|
-
*/
|
|
334
|
-
proposed?: boolean;
|
|
335
327
|
/**
|
|
336
328
|
* The claim was real but its source is gone — deleted or renamed upstream.
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
* `proposed`, so a model carrying deprecations is no longer wedged on
|
|
340
|
-
* unfixable `missing_file` findings.
|
|
329
|
+
* Verification skips source checks so a model carrying deprecations is no
|
|
330
|
+
* longer wedged on unfixable `missing_file` findings.
|
|
341
331
|
*
|
|
342
332
|
* This is a transitional marker, not a resting state. A cleanup pass removes
|
|
343
333
|
* the component and repairs the trails that referenced it; the field exists so
|
|
@@ -411,6 +401,37 @@ export interface SubsystemComponentEdge {
|
|
|
411
401
|
mechanism: SubsystemEdgeMechanism;
|
|
412
402
|
}
|
|
413
403
|
|
|
404
|
+
/**
|
|
405
|
+
* The construct that contains a seam site. The site is always in the step's
|
|
406
|
+
* own file/repo (`step.file` / `step.purl`), so it carries only the construct
|
|
407
|
+
* identity and its full declaration range.
|
|
408
|
+
*/
|
|
409
|
+
export interface SubsystemSiteConstruct {
|
|
410
|
+
/** Caller construct symbol (e.g. `MaintenanceModelList`, `Registry.start`). */
|
|
411
|
+
symbol: string;
|
|
412
|
+
/** Full declaration range of the construct, not just the seam line. */
|
|
413
|
+
span: SubsystemSourceSpan;
|
|
414
|
+
/** Declaration shape, when known (function, method, class, …). */
|
|
415
|
+
construct?: SubsystemConstruct;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* The construct a seam reaches. Unlike `site`, the target can live in another
|
|
420
|
+
* repo, so it carries its own purl + file. `span` is the target declaration
|
|
421
|
+
* range when it resolves; it can be absent for externals or unresolved targets.
|
|
422
|
+
*/
|
|
423
|
+
export interface SubsystemTargetConstruct {
|
|
424
|
+
/** File-anchored purl of the target repo/file. */
|
|
425
|
+
purl: string;
|
|
426
|
+
file: string;
|
|
427
|
+
/** Callee construct symbol (e.g. `mergeProvenance`, `Store.add`). */
|
|
428
|
+
symbol: string;
|
|
429
|
+
/** Full declaration range of the target, when resolvable. */
|
|
430
|
+
span?: SubsystemSourceSpan;
|
|
431
|
+
/** Declaration shape, when known. */
|
|
432
|
+
construct?: SubsystemConstruct;
|
|
433
|
+
}
|
|
434
|
+
|
|
414
435
|
export interface SubsystemTrailStep {
|
|
415
436
|
/** Source component alias. */
|
|
416
437
|
from: string;
|
|
@@ -428,6 +449,19 @@ export interface SubsystemTrailStep {
|
|
|
428
449
|
* unverified and the Trails view has no line to highlight.
|
|
429
450
|
*/
|
|
430
451
|
span?: SubsystemSourceSpan;
|
|
452
|
+
/**
|
|
453
|
+
* The construct containing the seam site, with its full declaration range.
|
|
454
|
+
* Required when `mechanism` is `calls`: the caller frame must be housed by
|
|
455
|
+
* the `from` component. For `uses` and data/event seams it is the via-site
|
|
456
|
+
* evidence and remains optional.
|
|
457
|
+
*/
|
|
458
|
+
site?: SubsystemSiteConstruct;
|
|
459
|
+
/**
|
|
460
|
+
* The construct the seam reaches. Distinct from `to` (a component alias):
|
|
461
|
+
* the target can be a method on a modeled component, or a construct that is
|
|
462
|
+
* not a node at all. Optional; populated from accepted call-site evidence.
|
|
463
|
+
*/
|
|
464
|
+
target?: SubsystemTargetConstruct;
|
|
431
465
|
/**
|
|
432
466
|
* File-anchored purl of the seam site (e.g.
|
|
433
467
|
* `pkg:github/owner/name#path/to/file.ts`), mirroring component `purl`.
|
|
@@ -441,14 +475,6 @@ export interface SubsystemTrailStep {
|
|
|
441
475
|
* mechanism + filename fallback.
|
|
442
476
|
*/
|
|
443
477
|
symbol: string;
|
|
444
|
-
/**
|
|
445
|
-
* Planned seam — the step describes intended behavior through code that is
|
|
446
|
-
* not written yet. Orthogonal to (and redundant with) the endpoints' own
|
|
447
|
-
* `proposed` flags: a step is proposed when it says so OR either endpoint
|
|
448
|
-
* is proposed. Verification skips source checks either way, and viewers
|
|
449
|
-
* tint it exactly like a proposed node.
|
|
450
|
-
*/
|
|
451
|
-
proposed?: boolean;
|
|
452
478
|
/**
|
|
453
479
|
* Free-text note anchored to this step's site line. Optional — informative
|
|
454
480
|
* only, never verified against source; viewers surface it via the codeview's
|
package/src/validation.test.ts
CHANGED
|
@@ -81,7 +81,70 @@ describe('validateSubsystemModelCrossField', () => {
|
|
|
81
81
|
expect(problems[0]!.path).toBe('/trails/0/steps/0/purl');
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
test('
|
|
84
|
+
test('flags a site construct that does not contain the seam span', () => {
|
|
85
|
+
const problems = validateSubsystemModelCrossField(
|
|
86
|
+
doc({
|
|
87
|
+
components: [comp('a'), comp('b')],
|
|
88
|
+
trails: [
|
|
89
|
+
{
|
|
90
|
+
id: 'w1',
|
|
91
|
+
title: 'flow',
|
|
92
|
+
steps: [
|
|
93
|
+
{
|
|
94
|
+
from: 'a',
|
|
95
|
+
to: 'b',
|
|
96
|
+
mechanism: 'calls',
|
|
97
|
+
file: 'src/a.ts',
|
|
98
|
+
purl: 'pkg:github/acme/app#src/a.ts',
|
|
99
|
+
symbol: 'a',
|
|
100
|
+
span: { startLine: 20, endLine: 20, contentHash: 'h' },
|
|
101
|
+
site: {
|
|
102
|
+
symbol: 'a',
|
|
103
|
+
span: { startLine: 1, endLine: 10, contentHash: 'h' },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
expect(problems.some((p) => p.path === '/trails/0/steps/0/site/span')).toBe(true);
|
|
112
|
+
expect(problems.find((p) => p.path === '/trails/0/steps/0/site/span')!.message).toContain(
|
|
113
|
+
'does not contain the seam span',
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('flags a target purl fragment that mismatches the target file', () => {
|
|
118
|
+
const problems = validateSubsystemModelCrossField(
|
|
119
|
+
doc({
|
|
120
|
+
components: [comp('a'), comp('b')],
|
|
121
|
+
trails: [
|
|
122
|
+
{
|
|
123
|
+
id: 'w1',
|
|
124
|
+
title: 'flow',
|
|
125
|
+
steps: [
|
|
126
|
+
{
|
|
127
|
+
from: 'a',
|
|
128
|
+
to: 'b',
|
|
129
|
+
mechanism: 'calls',
|
|
130
|
+
file: 'src/a.ts',
|
|
131
|
+
purl: 'pkg:github/acme/app#src/a.ts',
|
|
132
|
+
symbol: 'a',
|
|
133
|
+
target: {
|
|
134
|
+
purl: 'pkg:github/acme/app#src/b-other.ts',
|
|
135
|
+
file: 'src/b.ts',
|
|
136
|
+
symbol: 'b',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
expect(problems.some((p) => p.path === '/trails/0/steps/0/target/purl')).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('module implies file, exempting external', () => {
|
|
85
148
|
const bad = validateSubsystemModelCrossField(
|
|
86
149
|
doc({ components: [comp('a', { module: 'src/host', file: '' })] }),
|
|
87
150
|
);
|
|
@@ -93,7 +156,6 @@ describe('validateSubsystemModelCrossField', () => {
|
|
|
93
156
|
doc({
|
|
94
157
|
components: [
|
|
95
158
|
comp('ext', { module: 'pkg:npm/x', construct: 'external', file: '' }),
|
|
96
|
-
comp('plan', { module: 'src/host', file: '', proposed: true }),
|
|
97
159
|
comp('ok', { module: 'src/host', file: 'src/host/main.ts' }),
|
|
98
160
|
],
|
|
99
161
|
}),
|
|
@@ -126,7 +188,7 @@ describe('validateSubsystemModelCrossField', () => {
|
|
|
126
188
|
expect(problems[2]!.path).toBe('/components/2/file');
|
|
127
189
|
});
|
|
128
190
|
|
|
129
|
-
test('accepts
|
|
191
|
+
test('accepts an external identity and a node_modulesx directory', () => {
|
|
130
192
|
expect(
|
|
131
193
|
validateSubsystemModelCrossField(
|
|
132
194
|
doc({
|
|
@@ -137,8 +199,6 @@ describe('validateSubsystemModelCrossField', () => {
|
|
|
137
199
|
file: '',
|
|
138
200
|
purl: 'pkg:npm/@pierre/diffs',
|
|
139
201
|
}),
|
|
140
|
-
// A planned in-repo component may sit where it will live.
|
|
141
|
-
comp('plan', { file: 'packages/x/node_modules/y/z.ts', proposed: true }),
|
|
142
202
|
// `node_modulesx` is an ordinary directory, not the install root.
|
|
143
203
|
comp('ok', { file: 'packages/node_modulesx/z.ts' }),
|
|
144
204
|
],
|
package/src/validation.ts
CHANGED
|
@@ -25,14 +25,10 @@ export interface SubsystemValidationProblem {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function isUngrounded(c: SubsystemComponent): boolean {
|
|
28
|
-
// Deprecated components are exempt
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
return
|
|
32
|
-
c.construct === 'external' ||
|
|
33
|
-
c.proposed === true ||
|
|
34
|
-
c.deprecated === true
|
|
35
|
-
);
|
|
28
|
+
// Deprecated components are exempt: the file is a historical pointer to
|
|
29
|
+
// source that is gone, so it cannot resolve and its absence is not an
|
|
30
|
+
// inconsistency.
|
|
31
|
+
return c.construct === 'external' || c.deprecated === true;
|
|
36
32
|
}
|
|
37
33
|
|
|
38
34
|
/**
|
|
@@ -44,8 +40,7 @@ function isUngrounded(c: SubsystemComponent): boolean {
|
|
|
44
40
|
* Such a component is a third-party dependency: model it as `construct:
|
|
45
41
|
* 'external'` with `purl: 'pkg:npm/<package>'` and no file. Applies to externals
|
|
46
42
|
* too — they carry no file by design, so an install path there is dead weight
|
|
47
|
-
* that draws a link nothing can open.
|
|
48
|
-
* grounding rule: its file is a placeholder for something not placed yet.
|
|
43
|
+
* that draws a link nothing can open.
|
|
49
44
|
*
|
|
50
45
|
* The same rule covers trail step files — a seam into an external belongs
|
|
51
46
|
* at the call site in the caller, which *is* in the repo.
|
|
@@ -99,18 +94,17 @@ export function validateSubsystemModelCrossField(
|
|
|
99
94
|
ids.add(c.alias);
|
|
100
95
|
}
|
|
101
96
|
// A grounded component that claims a `module` needs a `file` to ground the
|
|
102
|
-
// frame. Externals
|
|
97
|
+
// frame. Externals carry no file by design.
|
|
103
98
|
const module = typeof c.module === 'string' ? c.module.trim() : '';
|
|
104
99
|
const file = typeof c.file === 'string' ? c.file.trim() : '';
|
|
105
100
|
if (module && !file && !isUngrounded(c)) {
|
|
106
101
|
problems.push({
|
|
107
102
|
path: `/components/${i}/module`,
|
|
108
|
-
message: `component ${JSON.stringify(c.alias)}: module ${JSON.stringify(module)} is set but file is empty — a module frame needs a file to ground it
|
|
103
|
+
message: `component ${JSON.stringify(c.alias)}: module ${JSON.stringify(module)} is set but file is empty — a module frame needs a file to ground it.`,
|
|
109
104
|
});
|
|
110
105
|
}
|
|
111
106
|
if (
|
|
112
107
|
file &&
|
|
113
|
-
c.proposed !== true &&
|
|
114
108
|
c.deprecated !== true &&
|
|
115
109
|
mentionsNodeModules(file)
|
|
116
110
|
) {
|
|
@@ -165,6 +159,56 @@ export function validateSubsystemModelCrossField(
|
|
|
165
159
|
`trail ${JSON.stringify(w.id)}: step ${si}`,
|
|
166
160
|
);
|
|
167
161
|
if (spanProblem) problems.push(spanProblem);
|
|
162
|
+
|
|
163
|
+
// The site construct is the enclosing frame; the seam line must live
|
|
164
|
+
// inside it. The schema can enforce each range but not containment.
|
|
165
|
+
const siteSpanProblem = spanRangeProblem(
|
|
166
|
+
step.site?.span,
|
|
167
|
+
`/trails/${ti}/steps/${si}/site/span`,
|
|
168
|
+
`trail ${JSON.stringify(w.id)}: step ${si} site`,
|
|
169
|
+
);
|
|
170
|
+
if (siteSpanProblem) problems.push(siteSpanProblem);
|
|
171
|
+
if (
|
|
172
|
+
step.span &&
|
|
173
|
+
step.site?.span &&
|
|
174
|
+
!(
|
|
175
|
+
step.site.span.startLine <= step.span.startLine &&
|
|
176
|
+
step.span.endLine <= step.site.span.endLine
|
|
177
|
+
)
|
|
178
|
+
) {
|
|
179
|
+
problems.push({
|
|
180
|
+
path: `/trails/${ti}/steps/${si}/site/span`,
|
|
181
|
+
message: `trail ${JSON.stringify(w.id)}: step ${si} site span ${step.site.span.startLine}-${step.site.span.endLine} does not contain the seam span ${step.span.startLine}-${step.span.endLine}.`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// The target is a construct in its own file, so its span and purl
|
|
186
|
+
// fragment are checked independently of the step's site file.
|
|
187
|
+
const targetSpanProblem = spanRangeProblem(
|
|
188
|
+
step.target?.span,
|
|
189
|
+
`/trails/${ti}/steps/${si}/target/span`,
|
|
190
|
+
`trail ${JSON.stringify(w.id)}: step ${si} target`,
|
|
191
|
+
);
|
|
192
|
+
if (targetSpanProblem) problems.push(targetSpanProblem);
|
|
193
|
+
if (
|
|
194
|
+
step.target &&
|
|
195
|
+
typeof step.target.purl === 'string' &&
|
|
196
|
+
step.target.purl.includes('#')
|
|
197
|
+
) {
|
|
198
|
+
const fragment = step.target.purl.split('#').slice(1).join('#');
|
|
199
|
+
if (fragment !== step.target.file) {
|
|
200
|
+
problems.push({
|
|
201
|
+
path: `/trails/${ti}/steps/${si}/target/purl`,
|
|
202
|
+
message: `trail ${JSON.stringify(w.id)}: step ${si} target purl fragment ${JSON.stringify(fragment)} does not match target file ${JSON.stringify(step.target.file)}`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (step.target && mentionsNodeModules(step.target.file)) {
|
|
207
|
+
problems.push({
|
|
208
|
+
path: `/trails/${ti}/steps/${si}/target/file`,
|
|
209
|
+
message: `trail ${JSON.stringify(w.id)}: step ${si} target file ${JSON.stringify(step.target.file)} points into node_modules — model the dependency as an external instead.`,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
168
212
|
});
|
|
169
213
|
});
|
|
170
214
|
|