@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.
@@ -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
- * The inverse of `proposed`: instead of "not built yet", this is "built once,
338
- * removed since". Verification skips source checks exactly as it does for
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
@@ -81,7 +81,70 @@ describe('validateSubsystemModelCrossField', () => {
81
81
  expect(problems[0]!.path).toBe('/trails/0/steps/0/purl');
82
82
  });
83
83
 
84
- test('module implies file, exempting external/proposed', () => {
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 node_modules only as an external/proposed component identity', () => {
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 for the same reason proposed ones are: the
29
- // file is a historical pointer to source that is gone, so it cannot resolve
30
- // and its absence is not an inconsistency.
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. `proposed` is exempt like every other
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 / proposed carry no file by design.
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 (mark the component proposed if it is not placed yet).`,
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