@principal-ai/subsystems-core 0.51.0 → 0.52.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/README.md +30 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +2 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +4 -1
- package/dist/node.js.map +1 -1
- package/dist/storage/topic-reference-links.d.ts +10 -0
- package/dist/storage/topic-reference-links.d.ts.map +1 -0
- package/dist/storage/topic-reference-links.js +41 -0
- package/dist/storage/topic-reference-links.js.map +1 -0
- package/dist/storage/topic-types.d.ts +18 -0
- package/dist/storage/topic-types.d.ts.map +1 -1
- package/dist/storage/topicStore.d.ts +1 -1
- package/dist/storage/topicStore.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +9 -0
- package/src/node.ts +2 -0
- package/src/storage/topic-reference-links.test.ts +47 -0
- package/src/storage/topic-reference-links.ts +38 -0
- package/src/storage/topic-types.ts +15 -0
- package/src/storage/topicStore.test.ts +51 -31
- package/src/storage/topicStore.ts +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Subsystem model types/schema and agent-session tooling for Principal AI.
|
|
|
6
6
|
|
|
7
7
|
| Import | Use |
|
|
8
8
|
|---|---|
|
|
9
|
-
| `@principal-ai/subsystems-core` | Browser-safe subsystem model types + `isSubsystemModelDocument` |
|
|
9
|
+
| `@principal-ai/subsystems-core` | Browser-safe subsystem model/topic types, reference links + `isSubsystemModelDocument` |
|
|
10
10
|
| `@principal-ai/subsystems-core/node` | OpenCode store, agent-session list/fetch/normalize, topic store, fixtures |
|
|
11
11
|
| `@principal-ai/subsystems-core/pipeline` | Bun-safe session normalize/accumulate (no `better-sqlite3`) |
|
|
12
12
|
|
|
@@ -14,6 +14,35 @@ Subsystem model types/schema and agent-session tooling for Principal AI.
|
|
|
14
14
|
|
|
15
15
|
Portable document schema: `schemas/subsystem-model.schema.json`
|
|
16
16
|
|
|
17
|
+
## Topic references
|
|
18
|
+
|
|
19
|
+
`DraftTopic.references` attaches local topics and C4 sketches by ID:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import {
|
|
23
|
+
formatTopicReferenceLink,
|
|
24
|
+
parseTopicReferenceLink,
|
|
25
|
+
type TopicReference,
|
|
26
|
+
} from '@principal-ai/subsystems-core';
|
|
27
|
+
|
|
28
|
+
const reference: TopicReference = { kind: 'c4-sketch', sketchId: 'c4s-123-abc' };
|
|
29
|
+
formatTopicReferenceLink(reference); // principal://c4-sketch/c4s-123-abc
|
|
30
|
+
parseTopicReferenceLink('principal://topic/topic-123-abc');
|
|
31
|
+
// { kind: 'topic', topicId: 'topic-123-abc' }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Create/update accepts `references`; updates replace the whole array (`[]` clears
|
|
35
|
+
it). Existing topics may omit the field. References are directional and local-only.
|
|
36
|
+
Targets stay separate artifacts; deleting a target leaves a missing reference,
|
|
37
|
+
not a cascading deletion. Existing `subsystemModels` associations are unchanged.
|
|
38
|
+
|
|
39
|
+
Markdown links such as `[Architecture](principal://c4-sketch/c4s-123-abc)` and
|
|
40
|
+
`[Related](principal://topic/topic-123-abc)` use the same target shapes but are
|
|
41
|
+
independent of attachments. Helpers only format/parse hrefs; hosts must allow the
|
|
42
|
+
`principal` protocol in Markdown sanitizers and handle resolution, navigation,
|
|
43
|
+
target validation, deduplication, and self-reference rejection. Sketch references
|
|
44
|
+
do not turn authored hypotheses into evidence.
|
|
45
|
+
|
|
17
46
|
## Installation
|
|
18
47
|
|
|
19
48
|
```bash
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* import { ... } from '@principal-ai/subsystems-core/pipeline'
|
|
12
12
|
*/
|
|
13
13
|
export * from './types';
|
|
14
|
+
export type { DraftTopic, TopicAsset, TopicCreator, TopicSubsystemModelRef, TopicReference, } from './storage/topic-types';
|
|
15
|
+
export { formatTopicReferenceLink, parseTopicReferenceLink } from './storage/topic-reference-links';
|
|
14
16
|
export { validateSubsystemModelCrossField } from './validation';
|
|
15
17
|
export type { SubsystemValidationProblem } from './validation';
|
|
16
18
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,cAAc,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,cAAc,SAAS,CAAC;AAExB,YAAY,EACV,UAAU,EACV,UAAU,EACV,YAAY,EACZ,sBAAsB,EACtB,cAAc,GACf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAKpG,OAAO,EAAE,gCAAgC,EAAE,MAAM,cAAc,CAAC;AAChE,YAAY,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -26,8 +26,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
26
26
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
27
27
|
};
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.validateSubsystemModelCrossField = void 0;
|
|
29
|
+
exports.validateSubsystemModelCrossField = exports.parseTopicReferenceLink = exports.formatTopicReferenceLink = void 0;
|
|
30
30
|
__exportStar(require("./types"), exports);
|
|
31
|
+
var topic_reference_links_1 = require("./storage/topic-reference-links");
|
|
32
|
+
Object.defineProperty(exports, "formatTopicReferenceLink", { enumerable: true, get: function () { return topic_reference_links_1.formatTopicReferenceLink; } });
|
|
33
|
+
Object.defineProperty(exports, "parseTopicReferenceLink", { enumerable: true, get: function () { return topic_reference_links_1.parseTopicReferenceLink; } });
|
|
31
34
|
// Cross-field validation (the rules the JSON Schema can't express). Structural
|
|
32
35
|
// (schema) validation is enforced per surface against
|
|
33
36
|
// `schemas/subsystem-model.schema.json`.
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;;;;;;;;;;;;;;;AAEH,0CAAwB;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;;;;;;;;;;;;;;;AAEH,0CAAwB;AASxB,yEAAoG;AAA3F,iIAAA,wBAAwB,OAAA;AAAE,gIAAA,uBAAuB,OAAA;AAE1D,+EAA+E;AAC/E,sDAAsD;AACtD,yCAAyC;AACzC,2CAAgE;AAAvD,8HAAA,gCAAgC,OAAA"}
|
package/dist/node.d.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
export * from './types';
|
|
10
10
|
export { TopicStore, PRINCIPAL_DIR, TOPICS_DIR, LEGACY_TOPICS_BLOB, } from './storage/topicStore';
|
|
11
11
|
export type { TopicIndexEntry, TopicCreate, TopicUpdate, MigrationResult, } from './storage/topicStore';
|
|
12
|
-
export type { DraftTopic, TopicAsset, TopicCreator, TopicSubsystemModelRef, } from './storage/topic-types';
|
|
12
|
+
export type { DraftTopic, TopicAsset, TopicCreator, TopicSubsystemModelRef, TopicReference, } from './storage/topic-types';
|
|
13
|
+
export { formatTopicReferenceLink, parseTopicReferenceLink } from './storage/topic-reference-links';
|
|
13
14
|
export { OpenCodeEventStore, defaultOpenCodeDBPath } from './opencode/OpenCodeEventStore';
|
|
14
15
|
export type { OpenCodeRawEvent, OpenCodeSessionEvents, OpenCodeEventRetriever, OpenCodeStoreOptions, SessionSummary, SessionListResult, } from './opencode/types';
|
|
15
16
|
export { listAgentSessions, detectAgent, fetchRawEvents } from './opencode/agent-sessions';
|
package/dist/node.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,SAAS,CAAC;AAGxB,OAAO,EACL,UAAU,EACV,aAAa,EACb,UAAU,EACV,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,eAAe,EACf,WAAW,EACX,WAAW,EACX,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,UAAU,EACV,UAAU,EACV,YAAY,EACZ,sBAAsB,
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,SAAS,CAAC;AAGxB,OAAO,EACL,UAAU,EACV,aAAa,EACb,UAAU,EACV,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,eAAe,EACf,WAAW,EACX,WAAW,EACX,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,UAAU,EACV,UAAU,EACV,YAAY,EACZ,sBAAsB,EACtB,cAAc,GACf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAGpG,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAC1F,YAAY,EACV,gBAAgB,EAChB,qBAAqB,EACrB,sBAAsB,EACtB,oBAAoB,EACpB,cAAc,EACd,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3F,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACpE,YAAY,EACV,mBAAmB,EACnB,uBAAuB,EACvB,uBAAuB,EACvB,0BAA0B,EAC1B,wBAAwB,EACxB,qBAAqB,EACrB,gBAAgB,GACjB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EACL,4BAA4B,EAC5B,eAAe,EACf,0BAA0B,EAC1B,gBAAgB,EAChB,mBAAmB,EACnB,6BAA6B,GAC9B,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/node.js
CHANGED
|
@@ -22,7 +22,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
22
22
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
23
23
|
};
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
-
exports.opencodeRowsToUniversalEvents = exports.collectRepositories = exports.accumulateEvents = exports.normalizeEventsWithAdapter = exports.normalizeEvents = exports.NodePathNormalizationAdapter = exports.buildAgentSessionFixture = exports.fetchRawEvents = exports.detectAgent = exports.listAgentSessions = exports.defaultOpenCodeDBPath = exports.OpenCodeEventStore = exports.LEGACY_TOPICS_BLOB = exports.TOPICS_DIR = exports.PRINCIPAL_DIR = exports.TopicStore = void 0;
|
|
25
|
+
exports.opencodeRowsToUniversalEvents = exports.collectRepositories = exports.accumulateEvents = exports.normalizeEventsWithAdapter = exports.normalizeEvents = exports.NodePathNormalizationAdapter = exports.buildAgentSessionFixture = exports.fetchRawEvents = exports.detectAgent = exports.listAgentSessions = exports.defaultOpenCodeDBPath = exports.OpenCodeEventStore = exports.parseTopicReferenceLink = exports.formatTopicReferenceLink = exports.LEGACY_TOPICS_BLOB = exports.TOPICS_DIR = exports.PRINCIPAL_DIR = exports.TopicStore = void 0;
|
|
26
26
|
__exportStar(require("./types"), exports);
|
|
27
27
|
// File-per-topic store (~/.principal/topics) — node:fs only
|
|
28
28
|
var topicStore_1 = require("./storage/topicStore");
|
|
@@ -30,6 +30,9 @@ Object.defineProperty(exports, "TopicStore", { enumerable: true, get: function (
|
|
|
30
30
|
Object.defineProperty(exports, "PRINCIPAL_DIR", { enumerable: true, get: function () { return topicStore_1.PRINCIPAL_DIR; } });
|
|
31
31
|
Object.defineProperty(exports, "TOPICS_DIR", { enumerable: true, get: function () { return topicStore_1.TOPICS_DIR; } });
|
|
32
32
|
Object.defineProperty(exports, "LEGACY_TOPICS_BLOB", { enumerable: true, get: function () { return topicStore_1.LEGACY_TOPICS_BLOB; } });
|
|
33
|
+
var topic_reference_links_1 = require("./storage/topic-reference-links");
|
|
34
|
+
Object.defineProperty(exports, "formatTopicReferenceLink", { enumerable: true, get: function () { return topic_reference_links_1.formatTopicReferenceLink; } });
|
|
35
|
+
Object.defineProperty(exports, "parseTopicReferenceLink", { enumerable: true, get: function () { return topic_reference_links_1.parseTopicReferenceLink; } });
|
|
33
36
|
// OpenCode event retrieval (Node.js only - SQLite)
|
|
34
37
|
var OpenCodeEventStore_1 = require("./opencode/OpenCodeEventStore");
|
|
35
38
|
Object.defineProperty(exports, "OpenCodeEventStore", { enumerable: true, get: function () { return OpenCodeEventStore_1.OpenCodeEventStore; } });
|
package/dist/node.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.js","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;;;;;;;;;;;;;;;AAEH,0CAAwB;AAExB,4DAA4D;AAC5D,mDAK8B;AAJ5B,wGAAA,UAAU,OAAA;AACV,2GAAA,aAAa,OAAA;AACb,wGAAA,UAAU,OAAA;AACV,gHAAA,kBAAkB,OAAA;AAepB,mDAAmD;AACnD,oEAA0F;AAAjF,wHAAA,kBAAkB,OAAA;AAAE,2HAAA,qBAAqB,OAAA;AAUlD,iEAAiE;AACjE,4DAA2F;AAAlF,mHAAA,iBAAiB,OAAA;AAAE,6GAAA,WAAW,OAAA;AAAE,gHAAA,cAAc,OAAA;AAOvD,mDAAmD;AACnD,oDAAoE;AAA3D,mHAAA,wBAAwB,OAAA;AAWjC,sEAAsE;AACtE,gDAO6B;AAN3B,wHAAA,4BAA4B,OAAA;AAC5B,2GAAA,eAAe,OAAA;AACf,sHAAA,0BAA0B,OAAA;AAC1B,4GAAA,gBAAgB,OAAA;AAChB,+GAAA,mBAAmB,OAAA;AACnB,yHAAA,6BAA6B,OAAA"}
|
|
1
|
+
{"version":3,"file":"node.js","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;;;;;;;;;;;;;;;AAEH,0CAAwB;AAExB,4DAA4D;AAC5D,mDAK8B;AAJ5B,wGAAA,UAAU,OAAA;AACV,2GAAA,aAAa,OAAA;AACb,wGAAA,UAAU,OAAA;AACV,gHAAA,kBAAkB,OAAA;AAepB,yEAAoG;AAA3F,iIAAA,wBAAwB,OAAA;AAAE,gIAAA,uBAAuB,OAAA;AAE1D,mDAAmD;AACnD,oEAA0F;AAAjF,wHAAA,kBAAkB,OAAA;AAAE,2HAAA,qBAAqB,OAAA;AAUlD,iEAAiE;AACjE,4DAA2F;AAAlF,mHAAA,iBAAiB,OAAA;AAAE,6GAAA,WAAW,OAAA;AAAE,gHAAA,cAAc,OAAA;AAOvD,mDAAmD;AACnD,oDAAoE;AAA3D,mHAAA,wBAAwB,OAAA;AAWjC,sEAAsE;AACtE,gDAO6B;AAN3B,wHAAA,4BAA4B,OAAA;AAC5B,2GAAA,eAAe,OAAA;AACf,sHAAA,0BAA0B,OAAA;AAC1B,4GAAA,gBAAgB,OAAA;AAChB,+GAAA,mBAAmB,OAAA;AACnB,yHAAA,6BAA6B,OAAA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TopicReference } from './topic-types';
|
|
2
|
+
/** Format a Markdown-safe href. Does not attach or resolve the target. */
|
|
3
|
+
export declare function formatTopicReferenceLink(reference: TopicReference): string;
|
|
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.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseTopicReferenceLink(href: string): TopicReference | null;
|
|
10
|
+
//# sourceMappingURL=topic-reference-links.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseTopicReferenceLink = exports.formatTopicReferenceLink = void 0;
|
|
4
|
+
// IDs are opaque single path segments, not paths. Decode exactly once and
|
|
5
|
+
// reject traversal, separators, whitespace, and control characters.
|
|
6
|
+
function isReferenceId(id) {
|
|
7
|
+
return id.length > 0 && id !== '.' && id !== '..' && !/[/\\\s\u0000-\u001f\u007f]/.test(id);
|
|
8
|
+
}
|
|
9
|
+
/** Format a Markdown-safe href. Does not attach or resolve the target. */
|
|
10
|
+
function formatTopicReferenceLink(reference) {
|
|
11
|
+
const id = reference.kind === 'topic' ? reference.topicId : reference.sketchId;
|
|
12
|
+
if (!isReferenceId(id))
|
|
13
|
+
throw new Error('Invalid topic reference id');
|
|
14
|
+
const encoded = encodeURIComponent(id).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
15
|
+
return `principal://${reference.kind}/${encoded}`;
|
|
16
|
+
}
|
|
17
|
+
exports.formatTopicReferenceLink = formatTopicReferenceLink;
|
|
18
|
+
/**
|
|
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.
|
|
22
|
+
*/
|
|
23
|
+
function parseTopicReferenceLink(href) {
|
|
24
|
+
const match = /^principal:\/\/(topic|c4-sketch)\/([^/?#]+)$/.exec(href);
|
|
25
|
+
if (!match)
|
|
26
|
+
return null;
|
|
27
|
+
let id;
|
|
28
|
+
try {
|
|
29
|
+
id = decodeURIComponent(match[2]);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (!isReferenceId(id))
|
|
35
|
+
return null;
|
|
36
|
+
return match[1] === 'topic'
|
|
37
|
+
? { kind: 'topic', topicId: id }
|
|
38
|
+
: { kind: 'c4-sketch', sketchId: id };
|
|
39
|
+
}
|
|
40
|
+
exports.parseTopicReferenceLink = parseTopicReferenceLink;
|
|
41
|
+
//# sourceMappingURL=topic-reference-links.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -55,6 +55,18 @@ export interface TopicSubsystemModelRef {
|
|
|
55
55
|
/** Specific runtime trails (flows) within the model, if narrower than the whole. */
|
|
56
56
|
trailIds?: string[];
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* A directional, local-only reference owned by a topic. Targets remain separate
|
|
60
|
+
* artifacts; their contents and titles are resolved by the host, not copied here.
|
|
61
|
+
* Referencing a C4 sketch does not make its authored hypothesis evidence.
|
|
62
|
+
*/
|
|
63
|
+
export type TopicReference = {
|
|
64
|
+
kind: 'c4-sketch';
|
|
65
|
+
sketchId: string;
|
|
66
|
+
} | {
|
|
67
|
+
kind: 'topic';
|
|
68
|
+
topicId: string;
|
|
69
|
+
};
|
|
58
70
|
/**
|
|
59
71
|
* Locally-authored topic — the shape desktop persists to
|
|
60
72
|
* `~/.principal/topics/<id>.json` and edits.
|
|
@@ -91,5 +103,11 @@ export interface DraftTopic {
|
|
|
91
103
|
* the referenced models are a local artifact.
|
|
92
104
|
*/
|
|
93
105
|
subsystemModels?: TopicSubsystemModelRef[];
|
|
106
|
+
/**
|
|
107
|
+
* Attached topics and C4 sketches. Whole-array replacement on update; `[]`
|
|
108
|
+
* clears attachments. Independent of links in `description`: neither edits
|
|
109
|
+
* the other. Hosts validate target existence and reject self-references.
|
|
110
|
+
*/
|
|
111
|
+
references?: TopicReference[];
|
|
94
112
|
}
|
|
95
113
|
//# sourceMappingURL=topic-types.d.ts.map
|
|
@@ -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;;;;;;;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;
|
|
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"}
|
|
@@ -64,7 +64,7 @@ export interface MigrationResult {
|
|
|
64
64
|
* Fields a caller may set when updating a topic. `id` and `createdAt` are set
|
|
65
65
|
* once at creation and cannot be changed by an update.
|
|
66
66
|
*/
|
|
67
|
-
export type TopicUpdate = Partial<Pick<DraftTopic, 'title' | 'description' | 'createdBy' | 'assets' | 'repos' | 'subsystemModels'>>;
|
|
67
|
+
export type TopicUpdate = Partial<Pick<DraftTopic, 'title' | 'description' | 'createdBy' | 'assets' | 'repos' | 'subsystemModels' | 'references'>>;
|
|
68
68
|
/**
|
|
69
69
|
* Fields a caller may set when creating a topic. `id`, `createdAt`,
|
|
70
70
|
* `updatedAt` are filled in when omitted; passing `id` lets a caller control
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topicStore.d.ts","sourceRoot":"","sources":["../../src/storage/topicStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,gEAAgE;AAChE,eAAO,MAAM,aAAa,QAAgC,CAAC;AAC3D,gCAAgC;AAChC,eAAO,MAAM,UAAU,QAAgC,CAAC;AACxD,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,QAAgD,CAAC;AAKhF;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,EAAE,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IACtD,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;CAClB;AA0CD,MAAM,WAAW,eAAe;IAC9B,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,YAAY,EAAE,OAAO,CAAC;CACvB;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,CAC/B,IAAI,CACF,UAAU,EACV,OAAO,GAAG,aAAa,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,iBAAiB,
|
|
1
|
+
{"version":3,"file":"topicStore.d.ts","sourceRoot":"","sources":["../../src/storage/topicStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,gEAAgE;AAChE,eAAO,MAAM,aAAa,QAAgC,CAAC;AAC3D,gCAAgC;AAChC,eAAO,MAAM,UAAU,QAAgC,CAAC;AACxD,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,QAAgD,CAAC;AAKhF;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,EAAE,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IACtD,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;CAClB;AA0CD,MAAM,WAAW,eAAe;IAC9B,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,YAAY,EAAE,OAAO,CAAC;CACvB;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,CAC/B,IAAI,CACF,UAAU,EACV,OAAO,GAAG,aAAa,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,iBAAiB,GAAG,YAAY,CAC9F,CACF,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC,GAAG;IAC7E,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,KAAK,CAA4B;IACzC,OAAO,CAAC,UAAU,CAAoC;IAEtD;;;;OAIG;gBACS,OAAO,GAAE,MAAmB,EAAE,cAAc,GAAE,MAA2B;IAQrF;;;;;;;;;OASG;IACG,SAAS,CAAC,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAUrE,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAOtD;;;;;OAKG;IACG,IAAI,CAAC,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAKrE,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAkBpD,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAclE,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAa/C;;;;;;;;;;OAUG;IACG,qBAAqB,IAAI,OAAO,CAAC,eAAe,CAAC;YAmEzC,QAAQ;YAQR,YAAY;IAQ1B,4EAA4E;YAC9D,UAAU;YAaV,SAAS;YAUT,cAAc;YAWd,UAAU;YASV,SAAS;IAkBvB,2EAA2E;YAC7D,YAAY;IA2B1B,OAAO,CAAC,YAAY;CAWrB"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
|
|
14
14
|
export * from './types';
|
|
15
15
|
|
|
16
|
+
export type {
|
|
17
|
+
DraftTopic,
|
|
18
|
+
TopicAsset,
|
|
19
|
+
TopicCreator,
|
|
20
|
+
TopicSubsystemModelRef,
|
|
21
|
+
TopicReference,
|
|
22
|
+
} from './storage/topic-types';
|
|
23
|
+
export { formatTopicReferenceLink, parseTopicReferenceLink } from './storage/topic-reference-links';
|
|
24
|
+
|
|
16
25
|
// Cross-field validation (the rules the JSON Schema can't express). Structural
|
|
17
26
|
// (schema) validation is enforced per surface against
|
|
18
27
|
// `schemas/subsystem-model.schema.json`.
|
package/src/node.ts
CHANGED
|
@@ -27,7 +27,9 @@ export type {
|
|
|
27
27
|
TopicAsset,
|
|
28
28
|
TopicCreator,
|
|
29
29
|
TopicSubsystemModelRef,
|
|
30
|
+
TopicReference,
|
|
30
31
|
} from './storage/topic-types';
|
|
32
|
+
export { formatTopicReferenceLink, parseTopicReferenceLink } from './storage/topic-reference-links';
|
|
31
33
|
|
|
32
34
|
// OpenCode event retrieval (Node.js only - SQLite)
|
|
33
35
|
export { OpenCodeEventStore, defaultOpenCodeDBPath } from './opencode/OpenCodeEventStore';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { formatTopicReferenceLink, parseTopicReferenceLink } from './topic-reference-links';
|
|
3
|
+
import type { TopicReference } from './topic-types';
|
|
4
|
+
|
|
5
|
+
describe('topic reference links', () => {
|
|
6
|
+
test('formats topic and C4 sketch links', () => {
|
|
7
|
+
expect(formatTopicReferenceLink({ kind: 'topic', topicId: 'topic-123-abc' }))
|
|
8
|
+
.toBe('principal://topic/topic-123-abc');
|
|
9
|
+
expect(formatTopicReferenceLink({ kind: 'c4-sketch', sketchId: 'c4s-123-abc' }))
|
|
10
|
+
.toBe('principal://c4-sketch/c4s-123-abc');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('round-trips opaque ids and escapes Markdown delimiters', () => {
|
|
14
|
+
const references: TopicReference[] = [
|
|
15
|
+
{ kind: 'topic', topicId: 'topic-123-abc' },
|
|
16
|
+
{ kind: 'c4-sketch', sketchId: 'c4s-123-abc' },
|
|
17
|
+
{ kind: 'topic', topicId: "topic-é(1)!'?#%" },
|
|
18
|
+
{ kind: 'topic', topicId: 'topic-%2F' },
|
|
19
|
+
];
|
|
20
|
+
for (const reference of references) {
|
|
21
|
+
const href = formatTopicReferenceLink(reference);
|
|
22
|
+
expect(href).not.toMatch(/[()!'?#]/);
|
|
23
|
+
expect(parseTopicReferenceLink(href)).toEqual(reference);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('rejects invalid ids when formatting', () => {
|
|
28
|
+
for (const topicId of ['', '.', '..', 'a/b', 'a\\b', ' a', 'a b', 'a\n', 'a\u0000']) {
|
|
29
|
+
expect(() => formatTopicReferenceLink({ kind: 'topic', topicId })).toThrow();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('returns null for unsupported or malformed hrefs', () => {
|
|
34
|
+
for (const href of [
|
|
35
|
+
'', 'https://example.com', 'pkg:github/acme/repo',
|
|
36
|
+
'principal://model/sg-1', 'principal://topic/', 'principal://topic',
|
|
37
|
+
'principal://topic/a/b', 'principal://topic/a/', 'principal://topic/a?q=1',
|
|
38
|
+
'principal://topic/a#heading', 'principal://topic/..', 'principal://topic/%2e%2e',
|
|
39
|
+
'principal://topic/%2F', 'principal://topic/%5C', 'principal://topic/%00',
|
|
40
|
+
'principal://topic/%20', 'principal://topic/a\n', 'principal://topic/%',
|
|
41
|
+
'principal://topic/%GG', 'principal://topic/%FF',
|
|
42
|
+
' principal://topic/a', 'principal://topic@host/a',
|
|
43
|
+
]) {
|
|
44
|
+
expect(parseTopicReferenceLink(href)).toBeNull();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { TopicReference } from './topic-types';
|
|
2
|
+
|
|
3
|
+
// IDs are opaque single path segments, not paths. Decode exactly once and
|
|
4
|
+
// reject traversal, separators, whitespace, and control characters.
|
|
5
|
+
function isReferenceId(id: string): boolean {
|
|
6
|
+
return id.length > 0 && id !== '.' && id !== '..' && !/[/\\\s\u0000-\u001f\u007f]/.test(id);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Format a Markdown-safe href. Does not attach or resolve the target. */
|
|
10
|
+
export function formatTopicReferenceLink(reference: TopicReference): string {
|
|
11
|
+
const id = reference.kind === 'topic' ? reference.topicId : reference.sketchId;
|
|
12
|
+
if (!isReferenceId(id)) throw new Error('Invalid topic reference id');
|
|
13
|
+
const encoded = encodeURIComponent(id).replace(
|
|
14
|
+
/[!'()*]/g,
|
|
15
|
+
(character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`,
|
|
16
|
+
);
|
|
17
|
+
return `principal://${reference.kind}/${encoded}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
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.
|
|
24
|
+
*/
|
|
25
|
+
export function parseTopicReferenceLink(href: string): TopicReference | null {
|
|
26
|
+
const match = /^principal:\/\/(topic|c4-sketch)\/([^/?#]+)$/.exec(href);
|
|
27
|
+
if (!match) return null;
|
|
28
|
+
let id: string;
|
|
29
|
+
try {
|
|
30
|
+
id = decodeURIComponent(match[2]);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (!isReferenceId(id)) return null;
|
|
35
|
+
return match[1] === 'topic'
|
|
36
|
+
? { kind: 'topic', topicId: id }
|
|
37
|
+
: { kind: 'c4-sketch', sketchId: id };
|
|
38
|
+
}
|
|
@@ -56,6 +56,15 @@ export interface TopicSubsystemModelRef {
|
|
|
56
56
|
trailIds?: string[];
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* A directional, local-only reference owned by a topic. Targets remain separate
|
|
61
|
+
* artifacts; their contents and titles are resolved by the host, not copied here.
|
|
62
|
+
* Referencing a C4 sketch does not make its authored hypothesis evidence.
|
|
63
|
+
*/
|
|
64
|
+
export type TopicReference =
|
|
65
|
+
| { kind: 'c4-sketch'; sketchId: string }
|
|
66
|
+
| { kind: 'topic'; topicId: string };
|
|
67
|
+
|
|
59
68
|
/**
|
|
60
69
|
* Locally-authored topic — the shape desktop persists to
|
|
61
70
|
* `~/.principal/topics/<id>.json` and edits.
|
|
@@ -92,4 +101,10 @@ export interface DraftTopic {
|
|
|
92
101
|
* the referenced models are a local artifact.
|
|
93
102
|
*/
|
|
94
103
|
subsystemModels?: TopicSubsystemModelRef[];
|
|
104
|
+
/**
|
|
105
|
+
* Attached topics and C4 sketches. Whole-array replacement on update; `[]`
|
|
106
|
+
* clears attachments. Independent of links in `description`: neither edits
|
|
107
|
+
* the other. Hosts validate target existence and reject self-references.
|
|
108
|
+
*/
|
|
109
|
+
references?: TopicReference[];
|
|
95
110
|
}
|
|
@@ -7,6 +7,7 @@ import { mkdtempSync, rmSync, writeFileSync, existsSync, readFileSync } from 'fs
|
|
|
7
7
|
import { tmpdir } from 'os';
|
|
8
8
|
import { join } from 'path';
|
|
9
9
|
import { TopicStore } from './topicStore';
|
|
10
|
+
import type { TopicReference } from './topic-types';
|
|
10
11
|
|
|
11
12
|
let dir: string;
|
|
12
13
|
let topicsDir: string;
|
|
@@ -91,6 +92,56 @@ describe('CRUD', () => {
|
|
|
91
92
|
expect(updated.subsystemModels).toEqual([{ modelId: 'sg-2-def' }]);
|
|
92
93
|
});
|
|
93
94
|
|
|
95
|
+
test('references survive fresh reads and unrelated updates, and can be replaced or cleared', async () => {
|
|
96
|
+
const store = makeStore();
|
|
97
|
+
const references: TopicReference[] = [
|
|
98
|
+
{ kind: 'c4-sketch', sketchId: 'c4s-1-abc' },
|
|
99
|
+
{ kind: 'topic', topicId: 'topic-related' },
|
|
100
|
+
];
|
|
101
|
+
const created = await store.createTopic({
|
|
102
|
+
id: 'topic-refs',
|
|
103
|
+
title: 'Architecture',
|
|
104
|
+
description: '[Related](principal://topic/topic-related)',
|
|
105
|
+
subsystemModels: [{ modelId: 'sg-1-abc' }],
|
|
106
|
+
references,
|
|
107
|
+
});
|
|
108
|
+
expect(created.references).toEqual(references);
|
|
109
|
+
expect((await makeStore().getTopic(created.id))?.references).toEqual(references);
|
|
110
|
+
|
|
111
|
+
await store.updateTopic(created.id, { title: 'Renamed' });
|
|
112
|
+
expect((await makeStore().getTopic(created.id))?.references).toEqual(references);
|
|
113
|
+
|
|
114
|
+
const replacement: TopicReference[] = [{ kind: 'topic', topicId: 'topic-other' }];
|
|
115
|
+
await store.updateTopic(created.id, { references: replacement });
|
|
116
|
+
expect((await makeStore().getTopic(created.id))?.references).toEqual(replacement);
|
|
117
|
+
|
|
118
|
+
await store.updateTopic(created.id, { references: [] });
|
|
119
|
+
expect(await makeStore().getTopic(created.id)).toMatchObject({
|
|
120
|
+
references: [],
|
|
121
|
+
description: created.description,
|
|
122
|
+
subsystemModels: created.subsystemModels,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('existing topics need no references migration', async () => {
|
|
127
|
+
const store = makeStore();
|
|
128
|
+
const topic = await store.createTopic({ title: 'Without attachments' });
|
|
129
|
+
expect(topic).not.toHaveProperty('references');
|
|
130
|
+
await store.updateTopic(topic.id, { description: 'Still without attachments' });
|
|
131
|
+
expect(await makeStore().getTopic(topic.id)).not.toHaveProperty('references');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('deleting a referenced topic preserves the attachment for missing-target display', async () => {
|
|
135
|
+
const store = makeStore();
|
|
136
|
+
const target = await store.createTopic({ title: 'Target' });
|
|
137
|
+
const owner = await store.createTopic({
|
|
138
|
+
title: 'Owner',
|
|
139
|
+
references: [{ kind: 'topic', topicId: target.id }],
|
|
140
|
+
});
|
|
141
|
+
await store.deleteTopic(target.id);
|
|
142
|
+
expect((await makeStore().getTopic(owner.id))?.references).toEqual(owner.references);
|
|
143
|
+
});
|
|
144
|
+
|
|
94
145
|
test('delete removes the file and the index entry', async () => {
|
|
95
146
|
const store = makeStore();
|
|
96
147
|
await store.createTopic({ id: 'topic-d', title: 'D' });
|
|
@@ -155,37 +206,6 @@ describe('index rebuild', () => {
|
|
|
155
206
|
'topic-external',
|
|
156
207
|
);
|
|
157
208
|
});
|
|
158
|
-
|
|
159
|
-
test('legacy topic files preserve unknown fields verbatim', async () => {
|
|
160
|
-
const store = makeStore();
|
|
161
|
-
await store.createTopic({
|
|
162
|
-
id: 'topic-legacy',
|
|
163
|
-
title: 'Legacy',
|
|
164
|
-
repos: ['pkg:github/a/b'],
|
|
165
|
-
});
|
|
166
|
-
const topicPath = join(topicsDir, 'topic-legacy.json');
|
|
167
|
-
const legacy = {
|
|
168
|
-
...JSON.parse(readFileSync(topicPath, 'utf8')),
|
|
169
|
-
trailIds: ['old-trail'],
|
|
170
|
-
status: { state: 'working', label: 'in flight' },
|
|
171
|
-
description: 'Keep this description',
|
|
172
|
-
};
|
|
173
|
-
writeFileSync(topicPath, JSON.stringify(legacy), 'utf8');
|
|
174
|
-
|
|
175
|
-
const loaded = await store.getTopic('topic-legacy');
|
|
176
|
-
|
|
177
|
-
expect(loaded).toMatchObject({
|
|
178
|
-
id: 'topic-legacy',
|
|
179
|
-
title: 'Legacy',
|
|
180
|
-
description: 'Keep this description',
|
|
181
|
-
repos: ['pkg:github/a/b'],
|
|
182
|
-
});
|
|
183
|
-
expect(loaded).toHaveProperty('trailIds', ['old-trail']);
|
|
184
|
-
// `status` was removed from the contract; legacy files still load and are
|
|
185
|
-
// read back verbatim until the desktop cleans them up.
|
|
186
|
-
expect(loaded).toHaveProperty('status', { state: 'working', label: 'in flight' });
|
|
187
|
-
expect(JSON.parse(readFileSync(topicPath, 'utf8'))).toHaveProperty('trailIds', ['old-trail']);
|
|
188
|
-
});
|
|
189
209
|
});
|
|
190
210
|
|
|
191
211
|
describe('migration', () => {
|
|
@@ -115,7 +115,7 @@ export interface MigrationResult {
|
|
|
115
115
|
export type TopicUpdate = Partial<
|
|
116
116
|
Pick<
|
|
117
117
|
DraftTopic,
|
|
118
|
-
'title' | 'description' | 'createdBy' | 'assets' | 'repos' | 'subsystemModels'
|
|
118
|
+
'title' | 'description' | 'createdBy' | 'assets' | 'repos' | 'subsystemModels' | 'references'
|
|
119
119
|
>
|
|
120
120
|
>;
|
|
121
121
|
|