@agentproto/corpus 0.1.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 agentproto contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # @agentproto/corpus
2
+
3
+ Canonical composition of AIP-10 / AIP-12 / AIP-18 / AIP-9 / AIP-15 / AIP-41 into an autonomous knowledge-improvement system.
4
+
5
+ **Status:** alpha — M0 (frozen fixture conformance) only.
6
+
7
+ ## What this is
8
+
9
+ A composition of existing AgentProto AIPs. It is **not** a new doctype or a parallel spec. It defines no new frontmatter shape of its own — every corpus-specific field rides under `metadata.corpus.*` on existing AIP frontmatters (until validated promotion to first-class via AIP amendments).
10
+
11
+ | AIP | Role in the corpus |
12
+ |---|---|
13
+ | AIP-10 KNOWLEDGE | Workspace manifest, raw sources, curated entries, faceted index, audit log |
14
+ | AIP-18 COLLECTION | Stateful operational records: candidates, gaps, reviews, eval-cases |
15
+ | AIP-12 PLAYBOOK | Executable, operator-bound procedural overlays (shadow/active/archived) |
16
+ | AIP-9 OPERATOR | Autonomous roles: scout, analyst, reviewer, curator, gap-finder |
17
+ | AIP-15 WORKFLOW | Source-to-corpus lifecycle workflows |
18
+ | AIP-41 ROUTINE | Scheduled autonomous loops |
19
+
20
+ ## Architectural invariants
21
+
22
+ This package is **pure**: imports only `@agentproto/*`, `zod`, `gray-matter`. Zero `@agstudio/*`, zero `@mastra/*`, zero `node:fs`, zero HTTP. All runtime concerns enter via injected ports under `src/ports/`.
23
+
24
+ The agstudio side (`packages/integration/knowledge/providers/corpus/`) supplies the concrete port implementations and registers a `CorpusKnowledgeAdapter implements IKnowledgeProvider` in the `KnowledgeProviderRegistry`.
25
+
26
+ ## M0 deliverable
27
+
28
+ A frozen reference fixture workspace under `test/fixtures/marketing/` that validates against the actual AgentProto JSON Schemas at `projects/agentproto/agentproto/specs/resources/aip-XX/draft/*.schema.json`. Engineers building later milestones copy from these fixtures instead of authoring fresh frontmatter.
29
+
30
+ Run `pnpm test` in this package to verify conformance.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @agentproto/corpus v0.1.0-alpha
3
+ * Composition of AIP-10/12/18/9/15/41 — autonomous knowledge-improvement kit.
4
+ */
5
+
6
+ // src/ports/clock.port.ts
7
+ var systemClock = {
8
+ now: () => /* @__PURE__ */ new Date(),
9
+ nowMs: () => Date.now()
10
+ };
11
+
12
+ export { systemClock };
13
+ //# sourceMappingURL=chunk-KYX2DTEH.mjs.map
14
+ //# sourceMappingURL=chunk-KYX2DTEH.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ports/clock.port.ts"],"names":[],"mappings":";;;;;;AAiBO,IAAM,WAAA,GAAyB;AAAA,EACpC,GAAA,EAAK,sBAAM,IAAI,IAAA,EAAK;AAAA,EACpB,KAAA,EAAO,MAAM,IAAA,CAAK,GAAA;AACpB","file":"chunk-KYX2DTEH.mjs","sourcesContent":["/**\n * ClockPort — abstracted time source.\n *\n * Tests need deterministic timestamps; production wants Date.now().\n * Every component that emits `*_at` fields or temporal scores takes\n * a ClockPort so the same code is testable + real.\n */\n\nexport interface ClockPort {\n /** Wall-clock now. */\n now(): Date\n\n /** Current ms since epoch. Equivalent to `now().getTime()`. */\n nowMs(): number\n}\n\n/** Real-time implementation. The local CLI host wires this in. */\nexport const systemClock: ClockPort = {\n now: () => new Date(),\n nowMs: () => Date.now(),\n}\n"]}
@@ -0,0 +1,104 @@
1
+ import { parse, stringify } from 'yaml';
2
+
3
+ /**
4
+ * @agentproto/corpus v0.1.0-alpha
5
+ * Composition of AIP-10/12/18/9/15/41 — autonomous knowledge-improvement kit.
6
+ */
7
+
8
+ var CandidatesSidecar = class {
9
+ constructor(opts) {
10
+ this.opts = opts;
11
+ }
12
+ /**
13
+ * Load every row from disk. Returns an empty list if the file
14
+ * doesn't exist (a fresh corpus has no candidates yet).
15
+ */
16
+ async load() {
17
+ if (!await this.opts.fs.exists(this.opts.path)) return [];
18
+ const content = await this.opts.fs.readFile(this.opts.path);
19
+ if (!content.trim()) return [];
20
+ const parsed = parse(content);
21
+ if (!parsed || !Array.isArray(parsed.candidates)) return [];
22
+ return parsed.candidates;
23
+ }
24
+ /**
25
+ * Append a new candidate. Refuses if `id` already exists — sidecar
26
+ * keys are unique. Returns the full updated list.
27
+ */
28
+ async append(row) {
29
+ const existing = await this.load();
30
+ if (existing.some((r) => r.id === row.id)) {
31
+ throw new SidecarDuplicateError(this.opts.path, row.id);
32
+ }
33
+ const next = [...existing, row];
34
+ await this.write(next);
35
+ return next;
36
+ }
37
+ /**
38
+ * Mutate one row in-place. Throws if not found.
39
+ */
40
+ async update(id, patch) {
41
+ const existing = await this.load();
42
+ let updated = null;
43
+ const next = existing.map((r) => {
44
+ if (r.id !== id) return r;
45
+ updated = { ...r, ...patch, id: r.id };
46
+ return updated;
47
+ });
48
+ if (!updated) throw new SidecarNotFoundError(this.opts.path, id);
49
+ await this.write(next);
50
+ return updated;
51
+ }
52
+ /**
53
+ * Remove a row by id. Returns the removed row so the caller can
54
+ * materialize an ITEM.md from its fields. Throws if not found.
55
+ *
56
+ * Use this when a candidate transitions out of `discovered`: pull
57
+ * it from the sidecar, write the ITEM.md, append an event.
58
+ */
59
+ async take(id) {
60
+ const existing = await this.load();
61
+ const idx = existing.findIndex((r) => r.id === id);
62
+ if (idx === -1) throw new SidecarNotFoundError(this.opts.path, id);
63
+ const removed = existing[idx];
64
+ const next = [...existing.slice(0, idx), ...existing.slice(idx + 1)];
65
+ await this.write(next);
66
+ return removed;
67
+ }
68
+ /**
69
+ * Whole-file replace. Used in tests and by the indexer for
70
+ * bulk migrations. Prefer append/update/take for normal flow.
71
+ */
72
+ async write(candidates) {
73
+ const shape = { candidates };
74
+ const content = stringify(shape, {
75
+ // Stable output: sort keys alphabetically across rows for
76
+ // greppable diffs. Each row's id key is preserved by the
77
+ // stringifier; the rest is dictionary order.
78
+ sortMapEntries: false
79
+ });
80
+ await this.opts.fs.writeFile(this.opts.path, content);
81
+ }
82
+ };
83
+ var SidecarDuplicateError = class extends Error {
84
+ constructor(path, id) {
85
+ super(
86
+ `SidecarDuplicateError: candidate id "${id}" already exists in ${path}`
87
+ );
88
+ this.path = path;
89
+ this.id = id;
90
+ this.name = "SidecarDuplicateError";
91
+ }
92
+ };
93
+ var SidecarNotFoundError = class extends Error {
94
+ constructor(path, id) {
95
+ super(`SidecarNotFoundError: candidate id "${id}" not in ${path}`);
96
+ this.path = path;
97
+ this.id = id;
98
+ this.name = "SidecarNotFoundError";
99
+ }
100
+ };
101
+
102
+ export { CandidatesSidecar, SidecarDuplicateError, SidecarNotFoundError };
103
+ //# sourceMappingURL=chunk-UPX26MYH.mjs.map
104
+ //# sourceMappingURL=chunk-UPX26MYH.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/workspace/sidecar.ts"],"names":["yamlParse","yamlStringify"],"mappings":"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","file":"chunk-UPX26MYH.mjs","sourcesContent":["/**\n * CandidatesSidecar — accessor for `collections/<name>/_candidates.yaml`.\n *\n * The plan calls out lazy materialization (so-so #6): the\n * \"discovered\" state has high volume and short lifespan; AIP-18 ITEM\n * files only materialize when a candidate transitions to `analyzed`.\n * Until then, candidates live in a YAML sidecar keyed by id.\n *\n * Sidecar shape:\n * candidates:\n * - id: <slug>\n * status: discovered # may also be transitioning (rare)\n * sourceUrl: ... # collection-specific fields are free-form\n * contentHash: ...\n * discoveredAt: ISO\n * discoveredBy: ws://operators/<slug>\n *\n * The accessor enforces:\n * - id uniqueness within the file\n * - atomic full-file writes (no partial sidecar)\n * - status transition discipline (a row stays here only while\n * `status === \"discovered\"`; transitions out remove the row and\n * return the data so the caller can materialize ITEM.md)\n */\n\nimport { parse as yamlParse, stringify as yamlStringify } from \"yaml\"\nimport type { FsPort } from \"../ports/fs.port.js\"\n\nexport interface CandidateRow {\n readonly id: string\n readonly status: \"discovered\" | string\n /** Open-ended — collection schema's `fields` defines what's allowed. */\n readonly [key: string]: unknown\n}\n\ninterface SidecarShape {\n readonly candidates: readonly CandidateRow[]\n}\n\nexport interface CandidatesSidecarOptions {\n readonly fs: FsPort\n /** Workspace-relative path of the sidecar file. */\n readonly path: string\n}\n\nexport class CandidatesSidecar {\n constructor(private readonly opts: CandidatesSidecarOptions) {}\n\n /**\n * Load every row from disk. Returns an empty list if the file\n * doesn't exist (a fresh corpus has no candidates yet).\n */\n async load(): Promise<readonly CandidateRow[]> {\n if (!(await this.opts.fs.exists(this.opts.path))) return []\n const content = await this.opts.fs.readFile(this.opts.path)\n if (!content.trim()) return []\n const parsed = yamlParse(content) as Partial<SidecarShape> | null\n if (!parsed || !Array.isArray(parsed.candidates)) return []\n return parsed.candidates as readonly CandidateRow[]\n }\n\n /**\n * Append a new candidate. Refuses if `id` already exists — sidecar\n * keys are unique. Returns the full updated list.\n */\n async append(row: CandidateRow): Promise<readonly CandidateRow[]> {\n const existing = await this.load()\n if (existing.some((r) => r.id === row.id)) {\n throw new SidecarDuplicateError(this.opts.path, row.id)\n }\n const next = [...existing, row]\n await this.write(next)\n return next\n }\n\n /**\n * Mutate one row in-place. Throws if not found.\n */\n async update(\n id: string,\n patch: Readonly<Record<string, unknown>>\n ): Promise<CandidateRow> {\n const existing = await this.load()\n let updated: CandidateRow | null = null\n const next = existing.map((r) => {\n if (r.id !== id) return r\n updated = { ...r, ...patch, id: r.id } as CandidateRow\n return updated\n })\n if (!updated) throw new SidecarNotFoundError(this.opts.path, id)\n await this.write(next)\n return updated\n }\n\n /**\n * Remove a row by id. Returns the removed row so the caller can\n * materialize an ITEM.md from its fields. Throws if not found.\n *\n * Use this when a candidate transitions out of `discovered`: pull\n * it from the sidecar, write the ITEM.md, append an event.\n */\n async take(id: string): Promise<CandidateRow> {\n const existing = await this.load()\n const idx = existing.findIndex((r) => r.id === id)\n if (idx === -1) throw new SidecarNotFoundError(this.opts.path, id)\n const removed = existing[idx]!\n const next = [...existing.slice(0, idx), ...existing.slice(idx + 1)]\n await this.write(next)\n return removed\n }\n\n /**\n * Whole-file replace. Used in tests and by the indexer for\n * bulk migrations. Prefer append/update/take for normal flow.\n */\n async write(candidates: readonly CandidateRow[]): Promise<void> {\n const shape: SidecarShape = { candidates }\n const content = yamlStringify(shape, {\n // Stable output: sort keys alphabetically across rows for\n // greppable diffs. Each row's id key is preserved by the\n // stringifier; the rest is dictionary order.\n sortMapEntries: false,\n })\n await this.opts.fs.writeFile(this.opts.path, content)\n }\n}\n\nexport class SidecarDuplicateError extends Error {\n constructor(\n readonly path: string,\n readonly id: string\n ) {\n super(\n `SidecarDuplicateError: candidate id \"${id}\" already exists in ${path}`\n )\n this.name = \"SidecarDuplicateError\"\n }\n}\n\nexport class SidecarNotFoundError extends Error {\n constructor(\n readonly path: string,\n readonly id: string\n ) {\n super(`SidecarNotFoundError: candidate id \"${id}\" not in ${path}`)\n this.name = \"SidecarNotFoundError\"\n }\n}\n"]}