@principal-ai/subsystems-core 0.51.1 → 0.51.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.
package/README.md CHANGED
@@ -16,7 +16,7 @@ Portable document schema: `schemas/subsystem-model.schema.json`
16
16
 
17
17
  ## Topic references
18
18
 
19
- `DraftTopic.references` attaches local topics and C4 sketches by ID:
19
+ `DraftTopic.references` attaches local topics, C4 sketches, and drawings by ID:
20
20
 
21
21
  ```ts
22
22
  import {
@@ -29,14 +29,21 @@ const reference: TopicReference = { kind: 'c4-sketch', sketchId: 'c4s-123-abc' }
29
29
  formatTopicReferenceLink(reference); // principal://c4-sketch/c4s-123-abc
30
30
  parseTopicReferenceLink('principal://topic/topic-123-abc');
31
31
  // { kind: 'topic', topicId: 'topic-123-abc' }
32
+ parseTopicReferenceLink('principal://drawing/0b6f7c1e');
33
+ // { kind: 'drawing', drawingId: '0b6f7c1e' }
32
34
  ```
33
35
 
36
+ A drawing ID is the file name, without extension, of a `.excalidraw` file in the
37
+ host's drawings folder (the desktop app uses `~/.alexandria/drawings/`). Titles
38
+ come from the drawing file's `appState.name` and are resolved by the host.
39
+
34
40
  Create/update accepts `references`; updates replace the whole array (`[]` clears
35
41
  it). Existing topics may omit the field. References are directional and local-only.
36
42
  Targets stay separate artifacts; deleting a target leaves a missing reference,
37
43
  not a cascading deletion. Existing `subsystemModels` associations are unchanged.
38
44
 
39
- Markdown links such as `[Architecture](principal://c4-sketch/c4s-123-abc)` and
45
+ Markdown links such as `[Architecture](principal://c4-sketch/c4s-123-abc)`,
46
+ `[Whiteboard](principal://drawing/0b6f7c1e)`, and
40
47
  `[Related](principal://topic/topic-123-abc)` use the same target shapes but are
41
48
  independent of attachments. Helpers only format/parse hrefs; hosts must allow the
42
49
  `principal` protocol in Markdown sanitizers and handle resolution, navigation,
@@ -2,9 +2,9 @@ import type { TopicReference } from './topic-types';
2
2
  /** Format a Markdown-safe href. Does not attach or resolve the target. */
3
3
  export declare function formatTopicReferenceLink(reference: TopicReference): string;
4
4
  /**
5
- * Parse a local topic/sketch href without I/O. Unknown kinds, malformed escapes,
6
- * extra path segments, queries, and fragments return null. Target existence and
7
- * navigation belong to the host. Parsing never creates an attachment.
5
+ * Parse a local topic/sketch/drawing href without I/O. Unknown kinds, malformed
6
+ * escapes, extra path segments, queries, and fragments return null. Target
7
+ * existence and navigation belong to the host. Parsing never creates an attachment.
8
8
  */
9
9
  export declare function parseTopicReferenceLink(href: string): TopicReference | null;
10
10
  //# sourceMappingURL=topic-reference-links.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"topic-reference-links.d.ts","sourceRoot":"","sources":["../../src/storage/topic-reference-links.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAQpD,0EAA0E;AAC1E,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,cAAc,GAAG,MAAM,CAQ1E;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAa3E"}
1
+ {"version":3,"file":"topic-reference-links.d.ts","sourceRoot":"","sources":["../../src/storage/topic-reference-links.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAmBpD,0EAA0E;AAC1E,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,cAAc,GAAG,MAAM,CAQ1E;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAkB3E"}
@@ -6,9 +6,19 @@ exports.parseTopicReferenceLink = exports.formatTopicReferenceLink = void 0;
6
6
  function isReferenceId(id) {
7
7
  return id.length > 0 && id !== '.' && id !== '..' && !/[/\\\s\u0000-\u001f\u007f]/.test(id);
8
8
  }
9
+ function referenceId(reference) {
10
+ switch (reference.kind) {
11
+ case 'topic':
12
+ return reference.topicId;
13
+ case 'c4-sketch':
14
+ return reference.sketchId;
15
+ case 'drawing':
16
+ return reference.drawingId;
17
+ }
18
+ }
9
19
  /** Format a Markdown-safe href. Does not attach or resolve the target. */
10
20
  function formatTopicReferenceLink(reference) {
11
- const id = reference.kind === 'topic' ? reference.topicId : reference.sketchId;
21
+ const id = referenceId(reference);
12
22
  if (!isReferenceId(id))
13
23
  throw new Error('Invalid topic reference id');
14
24
  const encoded = encodeURIComponent(id).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
@@ -16,12 +26,12 @@ function formatTopicReferenceLink(reference) {
16
26
  }
17
27
  exports.formatTopicReferenceLink = formatTopicReferenceLink;
18
28
  /**
19
- * Parse a local topic/sketch href without I/O. Unknown kinds, malformed escapes,
20
- * extra path segments, queries, and fragments return null. Target existence and
21
- * navigation belong to the host. Parsing never creates an attachment.
29
+ * Parse a local topic/sketch/drawing href without I/O. Unknown kinds, malformed
30
+ * escapes, extra path segments, queries, and fragments return null. Target
31
+ * existence and navigation belong to the host. Parsing never creates an attachment.
22
32
  */
23
33
  function parseTopicReferenceLink(href) {
24
- const match = /^principal:\/\/(topic|c4-sketch)\/([^/?#]+)$/.exec(href);
34
+ const match = /^principal:\/\/(topic|c4-sketch|drawing)\/([^/?#]+)$/.exec(href);
25
35
  if (!match)
26
36
  return null;
27
37
  let id;
@@ -33,9 +43,14 @@ function parseTopicReferenceLink(href) {
33
43
  }
34
44
  if (!isReferenceId(id))
35
45
  return null;
36
- return match[1] === 'topic'
37
- ? { kind: 'topic', topicId: id }
38
- : { kind: 'c4-sketch', sketchId: id };
46
+ switch (match[1]) {
47
+ case 'topic':
48
+ return { kind: 'topic', topicId: id };
49
+ case 'c4-sketch':
50
+ return { kind: 'c4-sketch', sketchId: id };
51
+ default:
52
+ return { kind: 'drawing', drawingId: id };
53
+ }
39
54
  }
40
55
  exports.parseTopicReferenceLink = parseTopicReferenceLink;
41
56
  //# sourceMappingURL=topic-reference-links.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"topic-reference-links.js","sourceRoot":"","sources":["../../src/storage/topic-reference-links.ts"],"names":[],"mappings":";;;AAEA,0EAA0E;AAC1E,oEAAoE;AACpE,SAAS,aAAa,CAAC,EAAU;IAC/B,OAAO,EAAE,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,IAAI,IAAI,CAAC,4BAA4B,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC9F,CAAC;AAED,0EAA0E;AAC1E,SAAgB,wBAAwB,CAAC,SAAyB;IAChE,MAAM,EAAE,GAAG,SAAS,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC;IAC/E,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;IACtE,MAAM,OAAO,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAAC,OAAO,CAC5C,UAAU,EACV,CAAC,SAAS,EAAE,EAAE,CAAC,IAAI,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,EAAE,CACxE,CAAC;IACF,OAAO,eAAe,SAAS,CAAC,IAAI,IAAI,OAAO,EAAE,CAAC;AACpD,CAAC;AARD,4DAQC;AAED;;;;GAIG;AACH,SAAgB,uBAAuB,CAAC,IAAY;IAClD,MAAM,KAAK,GAAG,8CAA8C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxE,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,IAAI,EAAU,CAAC;IACf,IAAI;QACF,EAAE,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;KACnC;IAAC,MAAM;QACN,OAAO,IAAI,CAAC;KACb;IACD,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO;QACzB,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE;QAChC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;AAC1C,CAAC;AAbD,0DAaC"}
1
+ {"version":3,"file":"topic-reference-links.js","sourceRoot":"","sources":["../../src/storage/topic-reference-links.ts"],"names":[],"mappings":";;;AAEA,0EAA0E;AAC1E,oEAAoE;AACpE,SAAS,aAAa,CAAC,EAAU;IAC/B,OAAO,EAAE,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,IAAI,IAAI,CAAC,4BAA4B,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC9F,CAAC;AAED,SAAS,WAAW,CAAC,SAAyB;IAC5C,QAAQ,SAAS,CAAC,IAAI,EAAE;QACtB,KAAK,OAAO;YACV,OAAO,SAAS,CAAC,OAAO,CAAC;QAC3B,KAAK,WAAW;YACd,OAAO,SAAS,CAAC,QAAQ,CAAC;QAC5B,KAAK,SAAS;YACZ,OAAO,SAAS,CAAC,SAAS,CAAC;KAC9B;AACH,CAAC;AAED,0EAA0E;AAC1E,SAAgB,wBAAwB,CAAC,SAAyB;IAChE,MAAM,EAAE,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IAClC,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;IACtE,MAAM,OAAO,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAAC,OAAO,CAC5C,UAAU,EACV,CAAC,SAAS,EAAE,EAAE,CAAC,IAAI,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,EAAE,CACxE,CAAC;IACF,OAAO,eAAe,SAAS,CAAC,IAAI,IAAI,OAAO,EAAE,CAAC;AACpD,CAAC;AARD,4DAQC;AAED;;;;GAIG;AACH,SAAgB,uBAAuB,CAAC,IAAY;IAClD,MAAM,KAAK,GAAG,sDAAsD,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChF,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,IAAI,EAAU,CAAC;IACf,IAAI;QACF,EAAE,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;KACnC;IAAC,MAAM;QACN,OAAO,IAAI,CAAC;KACb;IACD,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,QAAQ,KAAK,CAAC,CAAC,CAAC,EAAE;QAChB,KAAK,OAAO;YACV,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;QACxC,KAAK,WAAW;YACd,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;QAC7C;YACE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;KAC7C;AACH,CAAC;AAlBD,0DAkBC"}
@@ -59,10 +59,14 @@ export interface TopicSubsystemModelRef {
59
59
  * A directional, local-only reference owned by a topic. Targets remain separate
60
60
  * artifacts; their contents and titles are resolved by the host, not copied here.
61
61
  * Referencing a C4 sketch does not make its authored hypothesis evidence.
62
+ * A drawing ID is the `.excalidraw` file stem under the host's drawings folder.
62
63
  */
63
64
  export type TopicReference = {
64
65
  kind: 'c4-sketch';
65
66
  sketchId: string;
67
+ } | {
68
+ kind: 'drawing';
69
+ drawingId: string;
66
70
  } | {
67
71
  kind: 'topic';
68
72
  topicId: string;
@@ -104,7 +108,7 @@ export interface DraftTopic {
104
108
  */
105
109
  subsystemModels?: TopicSubsystemModelRef[];
106
110
  /**
107
- * Attached topics and C4 sketches. Whole-array replacement on update; `[]`
111
+ * Attached topics, C4 sketches, and drawings. Whole-array replacement on update; `[]`
108
112
  * clears attachments. Independent of links in `description`: neither edits
109
113
  * the other. Hosts validate target existence and reject self-references.
110
114
  */
@@ -1 +1 @@
1
- {"version":3,"file":"topic-types.d.ts","sourceRoot":"","sources":["../../src/storage/topic-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,kEAAkE;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,MAAM,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACtD;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,sBAAsB;IACrC,kDAAkD;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;GAIG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,mDAAmD;IACnD,EAAE,EAAE,MAAM,CAAC;IACX,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,8BAA8B;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,gBAAgB;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,mFAAmF;IACnF,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC;IACtB;;;OAGG;IACH,eAAe,CAAC,EAAE,sBAAsB,EAAE,CAAC;IAC3C;;;;OAIG;IACH,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;CAC/B"}
1
+ {"version":3,"file":"topic-types.d.ts","sourceRoot":"","sources":["../../src/storage/topic-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,kEAAkE;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,MAAM,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACtD;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,sBAAsB;IACrC,kDAAkD;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,mDAAmD;IACnD,EAAE,EAAE,MAAM,CAAC;IACX,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,8BAA8B;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,gBAAgB;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,mFAAmF;IACnF,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC;IACtB;;;OAGG;IACH,eAAe,CAAC,EAAE,sBAAsB,EAAE,CAAC;IAC3C;;;;OAIG;IACH,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;CAC/B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/subsystems-core",
3
- "version": "0.51.1",
3
+ "version": "0.51.2",
4
4
  "description": "Subsystem model types and agent-session tooling for Principal AI",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -3,17 +3,20 @@ import { formatTopicReferenceLink, parseTopicReferenceLink } from './topic-refer
3
3
  import type { TopicReference } from './topic-types';
4
4
 
5
5
  describe('topic reference links', () => {
6
- test('formats topic and C4 sketch links', () => {
6
+ test('formats topic, C4 sketch, and drawing links', () => {
7
7
  expect(formatTopicReferenceLink({ kind: 'topic', topicId: 'topic-123-abc' }))
8
8
  .toBe('principal://topic/topic-123-abc');
9
9
  expect(formatTopicReferenceLink({ kind: 'c4-sketch', sketchId: 'c4s-123-abc' }))
10
10
  .toBe('principal://c4-sketch/c4s-123-abc');
11
+ expect(formatTopicReferenceLink({ kind: 'drawing', drawingId: '0b6f7c1e-2a4d-4e8f-9c3b-1d2e3f4a5b6c' }))
12
+ .toBe('principal://drawing/0b6f7c1e-2a4d-4e8f-9c3b-1d2e3f4a5b6c');
11
13
  });
12
14
 
13
15
  test('round-trips opaque ids and escapes Markdown delimiters', () => {
14
16
  const references: TopicReference[] = [
15
17
  { kind: 'topic', topicId: 'topic-123-abc' },
16
18
  { kind: 'c4-sketch', sketchId: 'c4s-123-abc' },
19
+ { kind: 'drawing', drawingId: 'drawing-123' },
17
20
  { kind: 'topic', topicId: "topic-é(1)!'?#%" },
18
21
  { kind: 'topic', topicId: 'topic-%2F' },
19
22
  ];
@@ -28,6 +31,7 @@ describe('topic reference links', () => {
28
31
  for (const topicId of ['', '.', '..', 'a/b', 'a\\b', ' a', 'a b', 'a\n', 'a\u0000']) {
29
32
  expect(() => formatTopicReferenceLink({ kind: 'topic', topicId })).toThrow();
30
33
  }
34
+ expect(() => formatTopicReferenceLink({ kind: 'drawing', drawingId: '../d1' })).toThrow();
31
35
  });
32
36
 
33
37
  test('returns null for unsupported or malformed hrefs', () => {
@@ -40,6 +44,8 @@ describe('topic reference links', () => {
40
44
  'principal://topic/%20', 'principal://topic/a\n', 'principal://topic/%',
41
45
  'principal://topic/%GG', 'principal://topic/%FF',
42
46
  ' principal://topic/a', 'principal://topic@host/a',
47
+ 'principal://drawing/', 'principal://drawing/a/b', 'principal://drawing/..',
48
+ 'principal://drawings/a',
43
49
  ]) {
44
50
  expect(parseTopicReferenceLink(href)).toBeNull();
45
51
  }
@@ -6,9 +6,20 @@ function isReferenceId(id: string): boolean {
6
6
  return id.length > 0 && id !== '.' && id !== '..' && !/[/\\\s\u0000-\u001f\u007f]/.test(id);
7
7
  }
8
8
 
9
+ function referenceId(reference: TopicReference): string {
10
+ switch (reference.kind) {
11
+ case 'topic':
12
+ return reference.topicId;
13
+ case 'c4-sketch':
14
+ return reference.sketchId;
15
+ case 'drawing':
16
+ return reference.drawingId;
17
+ }
18
+ }
19
+
9
20
  /** Format a Markdown-safe href. Does not attach or resolve the target. */
10
21
  export function formatTopicReferenceLink(reference: TopicReference): string {
11
- const id = reference.kind === 'topic' ? reference.topicId : reference.sketchId;
22
+ const id = referenceId(reference);
12
23
  if (!isReferenceId(id)) throw new Error('Invalid topic reference id');
13
24
  const encoded = encodeURIComponent(id).replace(
14
25
  /[!'()*]/g,
@@ -18,12 +29,12 @@ export function formatTopicReferenceLink(reference: TopicReference): string {
18
29
  }
19
30
 
20
31
  /**
21
- * Parse a local topic/sketch href without I/O. Unknown kinds, malformed escapes,
22
- * extra path segments, queries, and fragments return null. Target existence and
23
- * navigation belong to the host. Parsing never creates an attachment.
32
+ * Parse a local topic/sketch/drawing href without I/O. Unknown kinds, malformed
33
+ * escapes, extra path segments, queries, and fragments return null. Target
34
+ * existence and navigation belong to the host. Parsing never creates an attachment.
24
35
  */
25
36
  export function parseTopicReferenceLink(href: string): TopicReference | null {
26
- const match = /^principal:\/\/(topic|c4-sketch)\/([^/?#]+)$/.exec(href);
37
+ const match = /^principal:\/\/(topic|c4-sketch|drawing)\/([^/?#]+)$/.exec(href);
27
38
  if (!match) return null;
28
39
  let id: string;
29
40
  try {
@@ -32,7 +43,12 @@ export function parseTopicReferenceLink(href: string): TopicReference | null {
32
43
  return null;
33
44
  }
34
45
  if (!isReferenceId(id)) return null;
35
- return match[1] === 'topic'
36
- ? { kind: 'topic', topicId: id }
37
- : { kind: 'c4-sketch', sketchId: id };
46
+ switch (match[1]) {
47
+ case 'topic':
48
+ return { kind: 'topic', topicId: id };
49
+ case 'c4-sketch':
50
+ return { kind: 'c4-sketch', sketchId: id };
51
+ default:
52
+ return { kind: 'drawing', drawingId: id };
53
+ }
38
54
  }
@@ -60,9 +60,11 @@ export interface TopicSubsystemModelRef {
60
60
  * A directional, local-only reference owned by a topic. Targets remain separate
61
61
  * artifacts; their contents and titles are resolved by the host, not copied here.
62
62
  * Referencing a C4 sketch does not make its authored hypothesis evidence.
63
+ * A drawing ID is the `.excalidraw` file stem under the host's drawings folder.
63
64
  */
64
65
  export type TopicReference =
65
66
  | { kind: 'c4-sketch'; sketchId: string }
67
+ | { kind: 'drawing'; drawingId: string }
66
68
  | { kind: 'topic'; topicId: string };
67
69
 
68
70
  /**
@@ -102,7 +104,7 @@ export interface DraftTopic {
102
104
  */
103
105
  subsystemModels?: TopicSubsystemModelRef[];
104
106
  /**
105
- * Attached topics and C4 sketches. Whole-array replacement on update; `[]`
107
+ * Attached topics, C4 sketches, and drawings. Whole-array replacement on update; `[]`
106
108
  * clears attachments. Independent of links in `description`: neither edits
107
109
  * the other. Hosts validate target existence and reject self-references.
108
110
  */
@@ -96,6 +96,7 @@ describe('CRUD', () => {
96
96
  const store = makeStore();
97
97
  const references: TopicReference[] = [
98
98
  { kind: 'c4-sketch', sketchId: 'c4s-1-abc' },
99
+ { kind: 'drawing', drawingId: 'drawing-1' },
99
100
  { kind: 'topic', topicId: 'topic-related' },
100
101
  ];
101
102
  const created = await store.createTopic({