@warpgogol/forge 2.8.0 → 2.8.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/AGENTS.md +1 -1
- package/README.md +23 -13
- package/README.uk.md +19 -12
- package/dist/os/session/handlers/validate.d.ts.map +1 -1
- package/dist/os/session/handlers/validate.js +23 -1
- package/dist/os/session/handlers/validate.js.map +1 -1
- package/dist/os/session/types.d.ts +31 -1
- package/dist/os/session/types.d.ts.map +1 -1
- package/dist/os/session/types.js +7 -0
- package/dist/os/session/types.js.map +1 -1
- package/os/adr/adr.module.ts +158 -0
- package/os/adr/frontmatter-io.ts +80 -0
- package/os/adr/handlers/archive.ts +244 -0
- package/os/adr/handlers/implement-stamp.ts +331 -0
- package/os/adr/handlers/list-create.ts +207 -0
- package/os/adr/handlers/validate.test.ts +232 -0
- package/os/adr/handlers/validate.ts +429 -0
- package/os/adr/index.ts +33 -0
- package/os/adr/types.ts +141 -0
- package/os/audit/audit.module.ts +48 -0
- package/os/audit/frontmatter-io.ts +84 -0
- package/os/audit/handlers/archive.ts +239 -0
- package/os/audit/index.ts +23 -0
- package/os/audit/types.ts +29 -0
- package/os/compass/compass.module.ts +186 -0
- package/os/compass/handlers/compass-audit-handler.ts +382 -0
- package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
- package/os/compass/handlers/compass-inventory-handler.ts +294 -0
- package/os/compass/handlers/compass-inventory.ts +521 -0
- package/os/compass/handlers/git-revision.ts +137 -0
- package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
- package/os/compass/handlers/resolve-scan-root.ts +79 -0
- package/os/compass/index.ts +33 -0
- package/os/core/core.module.ts +948 -0
- package/os/core/handlers/assets-check.ts +122 -0
- package/os/core/handlers/assets-helpers.ts +174 -0
- package/os/core/handlers/assets-list.ts +99 -0
- package/os/core/handlers/build.ts +155 -0
- package/os/core/handlers/determinism-check.ts +354 -0
- package/os/core/handlers/dev.ts +127 -0
- package/os/core/handlers/invariant-engine.test.ts +681 -0
- package/os/core/handlers/knowledge-compact.ts +248 -0
- package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
- package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
- package/os/core/handlers/note-link-validate.test.ts +117 -0
- package/os/core/handlers/note-orphan-detect.test.ts +94 -0
- package/os/core/handlers/package-health.test.ts +226 -0
- package/os/core/handlers/package-health.ts +231 -0
- package/os/core/handlers/pinned-check.ts +200 -0
- package/os/core/handlers/pinned-init.ts +333 -0
- package/os/core/handlers/pinned-types.ts +50 -0
- package/os/core/handlers/pinned-validate.ts +301 -0
- package/os/core/handlers/profile-resolve.ts +92 -0
- package/os/core/handlers/release-prepare.ts +294 -0
- package/os/core/handlers/release-publish.ts +211 -0
- package/os/core/handlers/validate.ts +262 -0
- package/os/core/handlers/workspace-deps.ts +55 -0
- package/os/core/index.ts +13 -0
- package/os/exploration/exploration.module.ts +90 -0
- package/os/exploration/frontmatter-io.ts +77 -0
- package/os/exploration/handlers/archive.ts +159 -0
- package/os/exploration/handlers/list.ts +77 -0
- package/os/exploration/handlers/show.ts +107 -0
- package/os/exploration/index.ts +25 -0
- package/os/exploration/types.ts +65 -0
- package/os/mission/handlers/archive.test.ts +391 -0
- package/os/mission/handlers/archive.ts +453 -0
- package/os/mission/index.ts +1 -0
- package/os/mission/mission.module.ts +48 -0
- package/os/mission/types.ts +41 -0
- package/os/naming/index.ts +14 -0
- package/os/naming/naming-convention.test.ts +163 -0
- package/os/naming/naming-convention.ts +378 -0
- package/os/naming/naming.module.ts +36 -0
- package/os/notes/index.ts +10 -0
- package/os/notes/notes.module.ts +109 -0
- package/os/plan/frontmatter-io.ts +82 -0
- package/os/plan/handlers/archive.ts +238 -0
- package/os/plan/index.ts +23 -0
- package/os/plan/plan.module.ts +48 -0
- package/os/plan/types.ts +29 -0
- package/os/program/discovery.ts +294 -0
- package/os/program/handlers/complete.ts +419 -0
- package/os/program/handlers/lease.ts +433 -0
- package/os/program/handlers/seal.ts +332 -0
- package/os/program/handlers/validate.ts +213 -0
- package/os/program/lease.ts +156 -0
- package/os/program/program.module.ts +212 -0
- package/os/program/schemas.ts +218 -0
- package/os/program/state.ts +264 -0
- package/os/rfc/acceptance.ts +344 -0
- package/os/rfc/decision-log.ts +310 -0
- package/os/rfc/dna-trace.ts +280 -0
- package/os/rfc/frontmatter-io.test.ts +132 -0
- package/os/rfc/frontmatter-io.ts +130 -0
- package/os/rfc/handlers/archive.ts +300 -0
- package/os/rfc/handlers/check.ts +238 -0
- package/os/rfc/handlers/implement-stamp.ts +515 -0
- package/os/rfc/handlers/index-graph.ts +237 -0
- package/os/rfc/handlers/lifecycle.test.ts +127 -0
- package/os/rfc/handlers/lifecycle.ts +249 -0
- package/os/rfc/handlers/list-create.ts +296 -0
- package/os/rfc/handlers/pipeline-status.ts +173 -0
- package/os/rfc/handlers/shared.ts +121 -0
- package/os/rfc/handlers/supersede-propose.ts +241 -0
- package/os/rfc/handlers/validate-rules.test.ts +891 -0
- package/os/rfc/handlers/validate-rules.ts +984 -0
- package/os/rfc/handlers/validate.ts +166 -0
- package/os/rfc/handlers.ts +19 -0
- package/os/rfc/index.ts +97 -0
- package/os/rfc/rfc.module.ts +416 -0
- package/os/rfc/types.ts +596 -0
- package/os/rfc/verification-evidence.ts +244 -0
- package/os/session/atif-parser.ts +166 -0
- package/os/session/frontmatter-io.ts +163 -0
- package/os/session/handlers/archive.ts +288 -0
- package/os/session/handlers/list.ts +174 -0
- package/os/session/handlers/save.ts +348 -0
- package/os/session/handlers/validate.ts +250 -0
- package/os/session/index.ts +50 -0
- package/os/session/session.module.ts +126 -0
- package/os/session/types.ts +203 -0
- package/os/spec/live-spec-list-show-validate.test.ts +257 -0
- package/os/spec/live-spec-list.ts +78 -0
- package/os/spec/live-spec-merge.test.ts +261 -0
- package/os/spec/live-spec-merge.ts +413 -0
- package/os/spec/live-spec-show.ts +117 -0
- package/os/spec/live-spec-types.ts +93 -0
- package/os/spec/live-spec-validate.ts +170 -0
- package/os/spec/spec-materialize.ts +390 -0
- package/os/spec/spec-schema.ts +174 -0
- package/os/spec/spec-status.ts +284 -0
- package/os/spec/spec-validate.ts +513 -0
- package/os/spec/spec.module.ts +135 -0
- package/os/werkstatt/handlers/lock.ts +168 -0
- package/os/werkstatt/handlers/schema.ts +47 -0
- package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
- package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
- package/os/werkstatt/index.ts +39 -0
- package/os/werkstatt/werkstatt.module.ts +62 -0
- package/os/workflow/handlers.ts +304 -0
- package/os/workflow/index.ts +28 -0
- package/os/workflow/types.ts +99 -0
- package/os/workflow/workflow.module.ts +54 -0
- package/package.json +3 -3
- package/profiles/godot-csharp.yaml +4 -4
- package/profiles/knowledge-typescript-turborepo.yaml +110 -0
- package/profiles/phaser-turborepo.yaml +5 -5
- package/skills/_shared/fo-session-summary.md +132 -16
- package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
- package/skills/fo/fo-doc-audit/SKILL.md +6 -6
- package/skills/fo/fo-handoff/SKILL.md +5 -0
- package/skills/fo/fo-idea/SKILL.md +1 -1
- package/skills/fo/fo-review/SKILL.md +1 -1
- package/skills/fo/fo-session-retro/SKILL.md +8 -0
- package/skills/fo/fo-session-save/SKILL.md +8 -0
- package/src/cli-output.ts +59 -0
- package/src/config/__tests__/resolve-terminology.test.ts +45 -0
- package/src/config/forge-config.ts +524 -0
- package/src/forge-module.ts +39 -0
- package/src/index.ts +172 -0
- package/src/knowledge/__tests__/promote.test.ts +219 -0
- package/src/knowledge/__tests__/serialize.test.ts +192 -0
- package/src/knowledge/budgets.ts +202 -0
- package/src/knowledge/compact.ts +405 -0
- package/src/knowledge/index.ts +53 -0
- package/src/knowledge/parse.ts +227 -0
- package/src/knowledge/promote.ts +156 -0
- package/src/knowledge/schema.ts +112 -0
- package/src/knowledge/serialize.ts +72 -0
- package/src/migration-adapters/__tests__/ignored-files.test.ts +51 -0
- package/src/migration-adapters/__tests__/registry.test.ts +131 -0
- package/src/migration-adapters/__tests__/types.test.ts +86 -0
- package/src/migration-adapters/git-utils.ts +71 -0
- package/src/migration-adapters/ignored-files.ts +221 -0
- package/src/migration-adapters/index.ts +21 -0
- package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
- package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
- package/src/migration-adapters/registry.ts +61 -0
- package/src/migration-adapters/types.ts +68 -0
- package/src/onboarding/__tests__/workspace-discovery.test.ts +134 -0
- package/src/onboarding/agents-generate.ts +443 -0
- package/src/onboarding/create.ts +427 -0
- package/src/onboarding/doctor.ts +1297 -0
- package/src/onboarding/init.ts +337 -0
- package/src/onboarding/invariant-engine.ts +448 -0
- package/src/onboarding/memory-scaffold.ts +189 -0
- package/src/onboarding/nested-agents-generate.ts +93 -0
- package/src/onboarding/nested-agents-templates.ts +233 -0
- package/src/onboarding/profile-validate.ts +139 -0
- package/src/onboarding/scaffold-project.ts +299 -0
- package/src/onboarding/scaffold.ts +143 -0
- package/src/onboarding/upgrade.ts +558 -0
- package/src/onboarding/workspace-discovery.ts +167 -0
- package/src/profiles/__tests__/profile-schema.test.ts +228 -0
- package/src/profiles/__tests__/stack-profile.test.ts +190 -0
- package/src/profiles/__tests__/terminology-utils.test.ts +71 -0
- package/src/profiles/profile-schema.ts +350 -0
- package/src/profiles/stack-profile.ts +188 -0
- package/src/profiles/terminology-utils.ts +36 -0
- package/src/registry.ts +185 -0
- package/src/skill-schema.ts +37 -0
- package/src/tests/acceptance-criteria.test.ts +99 -0
- package/src/tests/adr-implement-stamp.test.ts +368 -0
- package/src/tests/agents-generate-domain.test.ts +317 -0
- package/src/tests/agents-generate.test.ts +458 -0
- package/src/tests/bindings-schema.test.ts +314 -0
- package/src/tests/budgets.test.ts +213 -0
- package/src/tests/cli-output.test.ts +196 -0
- package/src/tests/compact.test.ts +796 -0
- package/src/tests/create.test.ts +289 -0
- package/src/tests/doctor-autonomy.test.ts +98 -0
- package/src/tests/doctor-bindings.test.ts +286 -0
- package/src/tests/doctor-domain.test.ts +113 -0
- package/src/tests/exploration-handlers.test.ts +256 -0
- package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
- package/src/tests/forge-config.test.ts +420 -0
- package/src/tests/fs-atomic.test.ts +96 -0
- package/src/tests/fs.test.ts +103 -0
- package/src/tests/generated-marker.test.ts +161 -0
- package/src/tests/hash.test.ts +37 -0
- package/src/tests/implement-stamp.test.ts +505 -0
- package/src/tests/init-bindings.test.ts +183 -0
- package/src/tests/knowledge-parse.test.ts +307 -0
- package/src/tests/knowledge-pbt.test.ts +164 -0
- package/src/tests/memory-scaffold.test.ts +192 -0
- package/src/tests/migration-adapters.test.ts +480 -0
- package/src/tests/package-files.test.ts +54 -0
- package/src/tests/pinned-check.test.ts +194 -0
- package/src/tests/pinned-init.test.ts +126 -0
- package/src/tests/profile-schema.test.ts +194 -0
- package/src/tests/profile-validate.test.ts +120 -0
- package/src/tests/program-lease.test.ts +245 -0
- package/src/tests/program-paths-extended.test.ts +117 -0
- package/src/tests/program-paths.test.ts +98 -0
- package/src/tests/program-property.test.ts +139 -0
- package/src/tests/program-schemas.test.ts +445 -0
- package/src/tests/program-spec-node.test.ts +250 -0
- package/src/tests/promote.test.ts +481 -0
- package/src/tests/registry.test.ts +73 -0
- package/src/tests/scaffold-project.test.ts +150 -0
- package/src/tests/session-handlers.test.ts +503 -0
- package/src/tests/session-pbt.test.ts +190 -0
- package/src/tests/skill-schema.test.ts +146 -0
- package/src/tests/skill-validate-knowledge.test.ts +26 -0
- package/src/tests/skill-validate.test.ts +254 -0
- package/src/tests/stack-profile.test.ts +221 -0
- package/src/tests/string-utils.test.ts +42 -0
- package/src/tests/upgrade.test.ts +428 -0
- package/src/tests/werkstatt-lock.test.ts +343 -0
- package/src/tests/workspace-discovery-domain.test.ts +112 -0
- package/src/tests/workspace-discovery.test.ts +135 -0
- package/src/types.ts +221 -0
- package/src/utils/__tests__/fs-idempotent.test.ts +73 -0
- package/src/utils/__tests__/fs-trash.test.ts +45 -0
- package/src/utils/fs-atomic.ts +93 -0
- package/src/utils/fs-idempotent.ts +41 -0
- package/src/utils/fs-trash-sync.ts +37 -0
- package/src/utils/fs-trash.ts +24 -0
- package/src/utils/fs.ts +74 -0
- package/src/utils/generated-marker.ts +166 -0
- package/src/utils/hash.ts +19 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/string-utils.ts +19 -0
- package/src/validators/__tests__/note-orphan-detect.test.ts +94 -0
- package/src/validators/note-frontmatter-validate.ts +142 -0
- package/src/validators/note-link-validate.ts +145 -0
- package/src/validators/note-orphan-detect.ts +146 -0
- package/src/validators/port-validate.ts +97 -0
- package/src/validators/skill-validate.ts +831 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Integration tests for forge.init binding defaults (RFC-0540).</purpose>
|
|
4
|
+
</MODULE_CONTRACT>
|
|
5
|
+
<CHANGE_SUMMARY>
|
|
6
|
+
<item>RFC-0540: initial init binding defaults integration tests.</item>
|
|
7
|
+
<item>RFC-0552: add test for Forge-vs-pack skill name conflict detection.</item>
|
|
8
|
+
</CHANGE_SUMMARY>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { test, expect, beforeEach, afterEach } from "vitest";
|
|
12
|
+
import { mkdtemp, mkdir, writeFile, rm, readFile } from "node:fs/promises";
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { runInit } from "../onboarding/init.ts";
|
|
17
|
+
import { loadForgeConfig } from "../config/forge-config.ts";
|
|
18
|
+
|
|
19
|
+
let tempDir: string;
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
tempDir = await mkdtemp(join(tmpdir(), "forge-init-bindings-test-"));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(async () => {
|
|
26
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
async function setupForgeSource(dir: string): Promise<void> {
|
|
30
|
+
await mkdir(join(dir, "packages", "forge", "skills"), { recursive: true });
|
|
31
|
+
await writeFile(
|
|
32
|
+
join(dir, "packages", "forge", "package.json"),
|
|
33
|
+
'{"name":"@warpgogol/forge"}',
|
|
34
|
+
"utf8",
|
|
35
|
+
);
|
|
36
|
+
await mkdir(join(dir, "packages", "forge", "os", "rfc"), { recursive: true });
|
|
37
|
+
await writeFile(
|
|
38
|
+
join(dir, "packages", "forge", "os", "rfc", "rfc-0000-template.md"),
|
|
39
|
+
"# RFC Template",
|
|
40
|
+
"utf8",
|
|
41
|
+
);
|
|
42
|
+
await mkdir(join(dir, "packages", "forge", "profiles"), { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test("forge.init creates forge.yaml with non-null forge-CLI bindings and null stack bindings", async () => {
|
|
46
|
+
await setupForgeSource(tempDir);
|
|
47
|
+
|
|
48
|
+
runInit({ flags: {} }, { workspaceRoot: tempDir });
|
|
49
|
+
|
|
50
|
+
const config = loadForgeConfig(tempDir);
|
|
51
|
+
expect(config.bindings).toBeDefined();
|
|
52
|
+
expect(config.bindings?.commands.validateRfc).toBe(
|
|
53
|
+
"pnpm exec forge rfc.validate --id {id} --json",
|
|
54
|
+
);
|
|
55
|
+
expect(config.bindings?.commands.validateAdr).toBe(
|
|
56
|
+
"pnpm exec forge adr.validate --id {id} --json",
|
|
57
|
+
);
|
|
58
|
+
expect(config.bindings?.commands.implementStamp).toBe(
|
|
59
|
+
"pnpm exec forge rfc.implement.stamp --id {id} --implementation-commit {commit}",
|
|
60
|
+
);
|
|
61
|
+
expect(config.bindings?.commands.specValidate).toBe(
|
|
62
|
+
"pnpm exec forge spec.validate --spec={id} --json",
|
|
63
|
+
);
|
|
64
|
+
expect(config.bindings?.commands.sessionSave).toBe("pnpm exec forge session.save --json");
|
|
65
|
+
expect(config.bindings?.commands.typecheck).toBeNull();
|
|
66
|
+
expect(config.bindings?.commands.test).toBeNull();
|
|
67
|
+
expect(config.bindings?.commands.scopedBuild).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("forge.yaml written by forge.init contains forge-CLI binding strings", async () => {
|
|
71
|
+
await setupForgeSource(tempDir);
|
|
72
|
+
|
|
73
|
+
runInit({ flags: {} }, { workspaceRoot: tempDir });
|
|
74
|
+
|
|
75
|
+
const yamlContent = await readFile(join(tempDir, "forge.yaml"), "utf8");
|
|
76
|
+
expect(yamlContent).toContain("forge rfc.validate");
|
|
77
|
+
expect(yamlContent).toContain("forge adr.validate");
|
|
78
|
+
expect(yamlContent).toContain("forge rfc.implement.stamp");
|
|
79
|
+
expect(yamlContent).toContain("forge spec.validate");
|
|
80
|
+
expect(yamlContent).toContain("forge session.save");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("forge.init does not overwrite existing forge.yaml", async () => {
|
|
84
|
+
await setupForgeSource(tempDir);
|
|
85
|
+
const existingYaml = `schema: forge/config@1
|
|
86
|
+
project:
|
|
87
|
+
name: existing
|
|
88
|
+
stack: []
|
|
89
|
+
packageManager: pnpm
|
|
90
|
+
paths:
|
|
91
|
+
rfcsDir: docs/rfcs
|
|
92
|
+
adrsDir: docs/adrs
|
|
93
|
+
plansDir: docs/plans
|
|
94
|
+
auditsDir: docs/audits
|
|
95
|
+
specsDir: docs/specs
|
|
96
|
+
skillsDir: .agents/skills
|
|
97
|
+
bindings:
|
|
98
|
+
schema: forge/bindings@1
|
|
99
|
+
commands:
|
|
100
|
+
validateRfc: "custom command"
|
|
101
|
+
validateAdr: null
|
|
102
|
+
implementStamp: null
|
|
103
|
+
typecheck: null
|
|
104
|
+
test: null
|
|
105
|
+
scopedBuild: null
|
|
106
|
+
specValidate: null
|
|
107
|
+
sessionSave: null
|
|
108
|
+
paths:
|
|
109
|
+
invariantsFile: null
|
|
110
|
+
compassDocs: []
|
|
111
|
+
reviewsDir: null
|
|
112
|
+
handoffsDir: null
|
|
113
|
+
sessionsDir: null
|
|
114
|
+
`;
|
|
115
|
+
await writeFile(join(tempDir, "forge.yaml"), existingYaml, "utf8");
|
|
116
|
+
|
|
117
|
+
const result = runInit({ flags: {} }, { workspaceRoot: tempDir });
|
|
118
|
+
expect(result.skipped).toContain("forge.yaml (already exists)");
|
|
119
|
+
|
|
120
|
+
const config = loadForgeConfig(tempDir);
|
|
121
|
+
expect(config.bindings?.commands.validateRfc).toBe("custom command");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("RFC-0552: forge.init returns skippedSkills field and syncs non-conflicting pack skills", async () => {
|
|
125
|
+
await setupForgeSource(tempDir);
|
|
126
|
+
|
|
127
|
+
// Create a pack with prefix 'wg' containing a non-conflicting skill
|
|
128
|
+
await mkdir(join(tempDir, "packages", "wg-skills", "skills", "wg-custom"), { recursive: true });
|
|
129
|
+
await writeFile(
|
|
130
|
+
join(tempDir, "packages", "wg-skills", "skills", "wg-custom", "SKILL.md"),
|
|
131
|
+
"---\nname: wg-custom\n---\n# wg-custom (should be synced)\n",
|
|
132
|
+
"utf8",
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Write forge.yaml with skillPacks config
|
|
136
|
+
const yaml = `schema: forge/config@1
|
|
137
|
+
project:
|
|
138
|
+
name: test-project
|
|
139
|
+
stack: []
|
|
140
|
+
packageManager: pnpm
|
|
141
|
+
paths:
|
|
142
|
+
rfcsDir: docs/rfcs
|
|
143
|
+
adrsDir: docs/adrs
|
|
144
|
+
plansDir: docs/plans
|
|
145
|
+
auditsDir: docs/audits
|
|
146
|
+
specsDir: docs/specs
|
|
147
|
+
skillsDir: .agents/skills
|
|
148
|
+
skillPacks:
|
|
149
|
+
- prefix: wg
|
|
150
|
+
dir: packages/wg-skills/skills
|
|
151
|
+
bindings:
|
|
152
|
+
schema: forge/bindings@1
|
|
153
|
+
commands:
|
|
154
|
+
validateRfc: null
|
|
155
|
+
validateAdr: null
|
|
156
|
+
implementStamp: null
|
|
157
|
+
typecheck: null
|
|
158
|
+
test: null
|
|
159
|
+
scopedBuild: null
|
|
160
|
+
specValidate: null
|
|
161
|
+
sessionSave: null
|
|
162
|
+
paths:
|
|
163
|
+
invariantsFile: null
|
|
164
|
+
compassDocs: []
|
|
165
|
+
reviewsDir: null
|
|
166
|
+
handoffsDir: null
|
|
167
|
+
sessionsDir: null
|
|
168
|
+
`;
|
|
169
|
+
await writeFile(join(tempDir, "forge.yaml"), yaml, "utf8");
|
|
170
|
+
|
|
171
|
+
const result = runInit(
|
|
172
|
+
{ flags: {} },
|
|
173
|
+
{ workspaceRoot: tempDir, forgeRoot: join(tempDir, "packages", "forge") },
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// skippedSkills field should exist and be empty (no conflicts)
|
|
177
|
+
expect(result.skippedSkills).toBeDefined();
|
|
178
|
+
expect(result.skippedSkills).toEqual([]);
|
|
179
|
+
|
|
180
|
+
// The non-conflicting pack skill should be synced
|
|
181
|
+
const wgSkillPath = join(tempDir, ".agents", "skills", "wg-custom", "SKILL.md");
|
|
182
|
+
expect(existsSync(wgSkillPath)).toBe(true);
|
|
183
|
+
});
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { test, expect, describe } from "vitest";
|
|
2
|
+
import { parseKnowledgeFile } from "../knowledge/parse.ts";
|
|
3
|
+
import { serializeKnowledgeFile } from "../knowledge/serialize.ts";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
|
|
8
|
+
function writeTempFile(content: string, fileName: string): string {
|
|
9
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-knowledge-"));
|
|
10
|
+
const filePath = path.join(tmpDir, fileName);
|
|
11
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
12
|
+
return filePath;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("RFC-0660: knowledge file parser", () => {
|
|
16
|
+
test("valid L0 entry with minimal metadata parses correctly", () => {
|
|
17
|
+
const content = `<!-- knowledge-layer: L0 -->
|
|
18
|
+
# Q&A Log
|
|
19
|
+
|
|
20
|
+
### K-0001: First question
|
|
21
|
+
|
|
22
|
+
\`\`\`knowledge-entry
|
|
23
|
+
id: K-0001
|
|
24
|
+
layer: L0
|
|
25
|
+
created: 2026-08-03
|
|
26
|
+
status: active
|
|
27
|
+
\`\`\`
|
|
28
|
+
|
|
29
|
+
- **Question:** Is this a test?
|
|
30
|
+
- **Answer:** Yes.
|
|
31
|
+
`;
|
|
32
|
+
const filePath = writeTempFile(content, "qa-log.md");
|
|
33
|
+
try {
|
|
34
|
+
const parsed = parseKnowledgeFile(filePath);
|
|
35
|
+
expect(parsed.isKnowledgeAdjacent).toBe(false);
|
|
36
|
+
expect(parsed.layer).toBe("L0");
|
|
37
|
+
expect(parsed.entries).toHaveLength(1);
|
|
38
|
+
expect(parsed.entries[0].meta.id).toBe("K-0001");
|
|
39
|
+
expect(parsed.entries[0].meta.layer).toBe("L0");
|
|
40
|
+
expect(parsed.entries[0].meta.status).toBe("active");
|
|
41
|
+
expect(parsed.entries[0].title).toBe("First question");
|
|
42
|
+
expect(parsed.entries[0].body).toContain("Is this a test?");
|
|
43
|
+
expect(parsed.parseIssues).toHaveLength(0);
|
|
44
|
+
expect(parsed.legacySections).toHaveLength(0);
|
|
45
|
+
} finally {
|
|
46
|
+
fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("valid L2 entry with confirmations and lastConfirmedAt parses correctly", () => {
|
|
51
|
+
const content = `<!-- knowledge-layer: L2 -->
|
|
52
|
+
# Learned Principles
|
|
53
|
+
|
|
54
|
+
### K-0001: Test principle
|
|
55
|
+
|
|
56
|
+
\`\`\`knowledge-entry
|
|
57
|
+
id: K-0001
|
|
58
|
+
layer: L2
|
|
59
|
+
created: 2026-08-03
|
|
60
|
+
lastConfirmedAt: 2026-08-03
|
|
61
|
+
confirmations: 3
|
|
62
|
+
status: active
|
|
63
|
+
\`\`\`
|
|
64
|
+
|
|
65
|
+
This is a confirmed principle.
|
|
66
|
+
`;
|
|
67
|
+
const filePath = writeTempFile(content, "learned-principles.md");
|
|
68
|
+
try {
|
|
69
|
+
const parsed = parseKnowledgeFile(filePath);
|
|
70
|
+
expect(parsed.entries).toHaveLength(1);
|
|
71
|
+
expect(parsed.entries[0].meta.confirmations).toBe(3);
|
|
72
|
+
expect(parsed.entries[0].meta.lastConfirmedAt).toBe("2026-08-03");
|
|
73
|
+
expect(parsed.parseIssues).toHaveLength(0);
|
|
74
|
+
} finally {
|
|
75
|
+
fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("L0 with forbidden confirmations produces parse issue", () => {
|
|
80
|
+
const content = `<!-- knowledge-layer: L0 -->
|
|
81
|
+
# Q&A Log
|
|
82
|
+
|
|
83
|
+
### K-0001: Bad entry
|
|
84
|
+
|
|
85
|
+
\`\`\`knowledge-entry
|
|
86
|
+
id: K-0001
|
|
87
|
+
layer: L0
|
|
88
|
+
created: 2026-08-03
|
|
89
|
+
confirmations: 5
|
|
90
|
+
status: active
|
|
91
|
+
\`\`\`
|
|
92
|
+
|
|
93
|
+
Body.
|
|
94
|
+
`;
|
|
95
|
+
const filePath = writeTempFile(content, "qa-log.md");
|
|
96
|
+
try {
|
|
97
|
+
const parsed = parseKnowledgeFile(filePath);
|
|
98
|
+
expect(parsed.parseIssues.length).toBeGreaterThan(0);
|
|
99
|
+
expect(parsed.parseIssues.some((i) => i.message.includes("confirmations"))).toBe(true);
|
|
100
|
+
} finally {
|
|
101
|
+
fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("L2 without required confirmations produces parse issue", () => {
|
|
106
|
+
const content = `<!-- knowledge-layer: L2 -->
|
|
107
|
+
# Learned Principles
|
|
108
|
+
|
|
109
|
+
### K-0001: Incomplete entry
|
|
110
|
+
|
|
111
|
+
\`\`\`knowledge-entry
|
|
112
|
+
id: K-0001
|
|
113
|
+
layer: L2
|
|
114
|
+
created: 2026-08-03
|
|
115
|
+
status: active
|
|
116
|
+
\`\`\`
|
|
117
|
+
|
|
118
|
+
Body.
|
|
119
|
+
`;
|
|
120
|
+
const filePath = writeTempFile(content, "learned-principles.md");
|
|
121
|
+
try {
|
|
122
|
+
const parsed = parseKnowledgeFile(filePath);
|
|
123
|
+
expect(parsed.parseIssues.length).toBeGreaterThan(0);
|
|
124
|
+
expect(parsed.parseIssues.some((i) => i.message.includes("confirmations is required"))).toBe(
|
|
125
|
+
true,
|
|
126
|
+
);
|
|
127
|
+
} finally {
|
|
128
|
+
fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("malformed YAML in metadata block produces parse issue, does not throw", () => {
|
|
133
|
+
const content = `<!-- knowledge-layer: L0 -->
|
|
134
|
+
# Q&A Log
|
|
135
|
+
|
|
136
|
+
### K-0001: Broken entry
|
|
137
|
+
|
|
138
|
+
\`\`\`knowledge-entry
|
|
139
|
+
id: K-0001
|
|
140
|
+
layer: L0
|
|
141
|
+
created: 2026-08-03
|
|
142
|
+
status: [unclosed
|
|
143
|
+
\`\`\`
|
|
144
|
+
|
|
145
|
+
Body.
|
|
146
|
+
`;
|
|
147
|
+
const filePath = writeTempFile(content, "qa-log.md");
|
|
148
|
+
try {
|
|
149
|
+
const parsed = parseKnowledgeFile(filePath);
|
|
150
|
+
expect(parsed.parseIssues.length).toBeGreaterThan(0);
|
|
151
|
+
expect(parsed.entries).toHaveLength(0);
|
|
152
|
+
} finally {
|
|
153
|
+
fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("knowledge-adjacent file (no K-NNNN headings, no layer preamble) returns empty result", () => {
|
|
158
|
+
const content = `# About Forge
|
|
159
|
+
|
|
160
|
+
This is a knowledge-adjacent file with no entries and no layer declaration.
|
|
161
|
+
It should be exempt from SKILL-19/SKILL-20.
|
|
162
|
+
`;
|
|
163
|
+
const filePath = writeTempFile(content, "forge-about.md");
|
|
164
|
+
try {
|
|
165
|
+
const parsed = parseKnowledgeFile(filePath);
|
|
166
|
+
expect(parsed.isKnowledgeAdjacent).toBe(true);
|
|
167
|
+
expect(parsed.entries).toHaveLength(0);
|
|
168
|
+
expect(parsed.legacySections).toHaveLength(0);
|
|
169
|
+
expect(parsed.parseIssues).toHaveLength(0);
|
|
170
|
+
} finally {
|
|
171
|
+
fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("file with layer but no entries is structured-empty (not knowledge-adjacent)", () => {
|
|
176
|
+
const content = `<!-- knowledge-layer: L0 -->
|
|
177
|
+
# Q&A Log
|
|
178
|
+
|
|
179
|
+
<!-- Entries are appended by the skill during runs. -->
|
|
180
|
+
`;
|
|
181
|
+
const filePath = writeTempFile(content, "qa-log.md");
|
|
182
|
+
try {
|
|
183
|
+
const parsed = parseKnowledgeFile(filePath);
|
|
184
|
+
expect(parsed.isKnowledgeAdjacent).toBe(false);
|
|
185
|
+
expect(parsed.layer).toBe("L0");
|
|
186
|
+
expect(parsed.entries).toHaveLength(0);
|
|
187
|
+
expect(parsed.legacySections).toHaveLength(0);
|
|
188
|
+
expect(parsed.parseIssues).toHaveLength(0);
|
|
189
|
+
} finally {
|
|
190
|
+
fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("layer detected via filename convention when no preamble comment", () => {
|
|
195
|
+
const content = `# Q&A Log
|
|
196
|
+
|
|
197
|
+
### K-0001: Test
|
|
198
|
+
|
|
199
|
+
\`\`\`knowledge-entry
|
|
200
|
+
id: K-0001
|
|
201
|
+
layer: L0
|
|
202
|
+
created: 2026-08-03
|
|
203
|
+
status: active
|
|
204
|
+
\`\`\`
|
|
205
|
+
|
|
206
|
+
Body.
|
|
207
|
+
`;
|
|
208
|
+
const filePath = writeTempFile(content, "qa-log.md");
|
|
209
|
+
try {
|
|
210
|
+
const parsed = parseKnowledgeFile(filePath);
|
|
211
|
+
expect(parsed.layer).toBe("L0");
|
|
212
|
+
} finally {
|
|
213
|
+
fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("layer comment overrides filename convention", () => {
|
|
218
|
+
const content = `<!-- knowledge-layer: L2 -->
|
|
219
|
+
# Custom File
|
|
220
|
+
|
|
221
|
+
### K-0001: Test
|
|
222
|
+
|
|
223
|
+
\`\`\`knowledge-entry
|
|
224
|
+
id: K-0001
|
|
225
|
+
layer: L2
|
|
226
|
+
created: 2026-08-03
|
|
227
|
+
lastConfirmedAt: 2026-08-03
|
|
228
|
+
confirmations: 1
|
|
229
|
+
status: active
|
|
230
|
+
\`\`\`
|
|
231
|
+
|
|
232
|
+
Body.
|
|
233
|
+
`;
|
|
234
|
+
const filePath = writeTempFile(content, "qa-log.md");
|
|
235
|
+
try {
|
|
236
|
+
const parsed = parseKnowledgeFile(filePath);
|
|
237
|
+
expect(parsed.layer).toBe("L2");
|
|
238
|
+
} finally {
|
|
239
|
+
fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("non-existent file returns empty result without throwing", () => {
|
|
244
|
+
const parsed = parseKnowledgeFile("/nonexistent/path/to/file.md");
|
|
245
|
+
expect(parsed.isKnowledgeAdjacent).toBe(true);
|
|
246
|
+
expect(parsed.entries).toHaveLength(0);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("directory path returns empty result without throwing", () => {
|
|
250
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-knowledge-dir-"));
|
|
251
|
+
try {
|
|
252
|
+
const parsed = parseKnowledgeFile(tmpDir);
|
|
253
|
+
expect(parsed.isKnowledgeAdjacent).toBe(true);
|
|
254
|
+
expect(parsed.entries).toHaveLength(0);
|
|
255
|
+
} finally {
|
|
256
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("RFC-0660: knowledge file serializer round-trip", () => {
|
|
262
|
+
test("parse → serialize → parse produces identical metadata", () => {
|
|
263
|
+
const content = `<!-- knowledge-layer: L0 -->
|
|
264
|
+
# Q&A Log
|
|
265
|
+
|
|
266
|
+
### K-0001: First question
|
|
267
|
+
|
|
268
|
+
\`\`\`knowledge-entry
|
|
269
|
+
id: K-0001
|
|
270
|
+
layer: L0
|
|
271
|
+
created: 2026-08-03
|
|
272
|
+
status: active
|
|
273
|
+
\`\`\`
|
|
274
|
+
|
|
275
|
+
- **Question:** Is this a test?
|
|
276
|
+
- **Answer:** Yes.
|
|
277
|
+
|
|
278
|
+
### K-0002: Second question
|
|
279
|
+
|
|
280
|
+
\`\`\`knowledge-entry
|
|
281
|
+
id: K-0002
|
|
282
|
+
layer: L0
|
|
283
|
+
created: 2026-08-03
|
|
284
|
+
status: active
|
|
285
|
+
\`\`\`
|
|
286
|
+
|
|
287
|
+
- **Question:** Another question?
|
|
288
|
+
- **Answer:** Another answer.
|
|
289
|
+
`;
|
|
290
|
+
const filePath = writeTempFile(content, "qa-log.md");
|
|
291
|
+
try {
|
|
292
|
+
const parsed1 = parseKnowledgeFile(filePath);
|
|
293
|
+
const serialized = serializeKnowledgeFile(parsed1);
|
|
294
|
+
const filePath2 = writeTempFile(serialized, "qa-log.md");
|
|
295
|
+
const parsed2 = parseKnowledgeFile(filePath2);
|
|
296
|
+
|
|
297
|
+
expect(parsed2.entries).toHaveLength(parsed1.entries.length);
|
|
298
|
+
for (let i = 0; i < parsed1.entries.length; i++) {
|
|
299
|
+
expect(parsed2.entries[i].meta).toEqual(parsed1.entries[i].meta);
|
|
300
|
+
expect(parsed2.entries[i].title).toBe(parsed1.entries[i].title);
|
|
301
|
+
}
|
|
302
|
+
fs.rmSync(path.dirname(filePath2), { recursive: true, force: true });
|
|
303
|
+
} finally {
|
|
304
|
+
fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { test, expect, describe } from "vitest";
|
|
2
|
+
import { parseKnowledgeFile } from "../knowledge/parse.ts";
|
|
3
|
+
import { serializeKnowledgeFile } from "../knowledge/serialize.ts";
|
|
4
|
+
import { knowledgeEntryMetaSchema, type KnowledgeEntryMeta } from "../knowledge/schema.ts";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import fc from "fast-check";
|
|
9
|
+
|
|
10
|
+
function buildMarkdown(entries: KnowledgeEntryMeta[], layer: string, fileName: string): string {
|
|
11
|
+
const parts: string[] = [`<!-- knowledge-layer: ${layer} -->`, `# ${fileName}`, ""];
|
|
12
|
+
for (const meta of entries) {
|
|
13
|
+
parts.push(`### ${meta.id}: Test entry`);
|
|
14
|
+
parts.push("");
|
|
15
|
+
parts.push("```knowledge-entry");
|
|
16
|
+
const fields: [string, unknown][] = [
|
|
17
|
+
["id", meta.id],
|
|
18
|
+
["layer", meta.layer],
|
|
19
|
+
["created", meta.created],
|
|
20
|
+
];
|
|
21
|
+
if (meta.lastConfirmedAt !== undefined) fields.push(["lastConfirmedAt", meta.lastConfirmedAt]);
|
|
22
|
+
if (meta.confirmations !== undefined) fields.push(["confirmations", meta.confirmations]);
|
|
23
|
+
if (meta.expiresAt !== undefined) fields.push(["expiresAt", meta.expiresAt]);
|
|
24
|
+
if (meta.supersedes !== undefined) fields.push(["supersedes", meta.supersedes]);
|
|
25
|
+
if (meta.promotedTo !== undefined) fields.push(["promotedTo", meta.promotedTo]);
|
|
26
|
+
fields.push(["status", meta.status]);
|
|
27
|
+
for (const [key, value] of fields) {
|
|
28
|
+
if (value === null) {
|
|
29
|
+
parts.push(`${key}: null`);
|
|
30
|
+
} else if (Array.isArray(value)) {
|
|
31
|
+
parts.push(`${key}: [${value.join(", ")}]`);
|
|
32
|
+
} else {
|
|
33
|
+
parts.push(`${key}: ${value}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
parts.push("```");
|
|
37
|
+
parts.push("");
|
|
38
|
+
parts.push("Body text for this entry.");
|
|
39
|
+
parts.push("");
|
|
40
|
+
}
|
|
41
|
+
return parts.join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeTempFile(content: string, fileName: string): string {
|
|
45
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-knowledge-pbt-"));
|
|
46
|
+
const filePath = path.join(tmpDir, fileName);
|
|
47
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
48
|
+
return filePath;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("RFC-0660: property-based round-trip", () => {
|
|
52
|
+
test("parse → serialize → parse produces identical metadata for L0 entries", () => {
|
|
53
|
+
const l0EntryArbitrary = fc.integer({ min: 1, max: 9999 }).map((n) => ({
|
|
54
|
+
id: `K-${String(n).padStart(4, "0")}`,
|
|
55
|
+
layer: "L0" as const,
|
|
56
|
+
created: "2026-08-03",
|
|
57
|
+
status: "active" as const,
|
|
58
|
+
})) as fc.Arbitrary<KnowledgeEntryMeta>;
|
|
59
|
+
|
|
60
|
+
fc.assert(
|
|
61
|
+
fc.property(fc.array(l0EntryArbitrary, { minLength: 1, maxLength: 5 }), (entries) => {
|
|
62
|
+
const markdown = buildMarkdown(entries, "L0", "qa-log.md");
|
|
63
|
+
const filePath1 = writeTempFile(markdown, "qa-log.md");
|
|
64
|
+
try {
|
|
65
|
+
const parsed1 = parseKnowledgeFile(filePath1);
|
|
66
|
+
const serialized = serializeKnowledgeFile(parsed1);
|
|
67
|
+
const filePath2 = writeTempFile(serialized, "qa-log.md");
|
|
68
|
+
try {
|
|
69
|
+
const parsed2 = parseKnowledgeFile(filePath2);
|
|
70
|
+
expect(parsed2.entries.length).toBe(parsed1.entries.length);
|
|
71
|
+
for (let i = 0; i < parsed1.entries.length; i++) {
|
|
72
|
+
expect(parsed2.entries[i].meta).toEqual(parsed1.entries[i].meta);
|
|
73
|
+
}
|
|
74
|
+
} finally {
|
|
75
|
+
fs.rmSync(path.dirname(filePath2), { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
} finally {
|
|
78
|
+
fs.rmSync(path.dirname(filePath1), { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
}),
|
|
81
|
+
{ numRuns: 50 },
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("parse → serialize → parse produces identical metadata for L2 entries", () => {
|
|
86
|
+
const l2EntryArbitrary = fc.integer({ min: 1, max: 9999 }).map((n) => ({
|
|
87
|
+
id: `K-${String(n).padStart(4, "0")}`,
|
|
88
|
+
layer: "L2" as const,
|
|
89
|
+
created: "2026-08-03",
|
|
90
|
+
lastConfirmedAt: "2026-08-03",
|
|
91
|
+
confirmations: n % 100,
|
|
92
|
+
status: "active" as const,
|
|
93
|
+
})) as fc.Arbitrary<KnowledgeEntryMeta>;
|
|
94
|
+
|
|
95
|
+
fc.assert(
|
|
96
|
+
fc.property(fc.array(l2EntryArbitrary, { minLength: 1, maxLength: 5 }), (entries) => {
|
|
97
|
+
const markdown = buildMarkdown(entries, "L2", "learned-principles.md");
|
|
98
|
+
const filePath1 = writeTempFile(markdown, "learned-principles.md");
|
|
99
|
+
try {
|
|
100
|
+
const parsed1 = parseKnowledgeFile(filePath1);
|
|
101
|
+
const serialized = serializeKnowledgeFile(parsed1);
|
|
102
|
+
const filePath2 = writeTempFile(serialized, "learned-principles.md");
|
|
103
|
+
try {
|
|
104
|
+
const parsed2 = parseKnowledgeFile(filePath2);
|
|
105
|
+
expect(parsed2.entries.length).toBe(parsed1.entries.length);
|
|
106
|
+
for (let i = 0; i < parsed1.entries.length; i++) {
|
|
107
|
+
expect(parsed2.entries[i].meta).toEqual(parsed1.entries[i].meta);
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
fs.rmSync(path.dirname(filePath2), { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
} finally {
|
|
113
|
+
fs.rmSync(path.dirname(filePath1), { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
}),
|
|
116
|
+
{ numRuns: 50 },
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("schema validates well-formed L0 entry", () => {
|
|
121
|
+
const validL0 = {
|
|
122
|
+
id: "K-0001",
|
|
123
|
+
layer: "L0",
|
|
124
|
+
created: "2026-08-03",
|
|
125
|
+
status: "active",
|
|
126
|
+
};
|
|
127
|
+
expect(knowledgeEntryMetaSchema.safeParse(validL0).success).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("schema validates well-formed L2 entry with confirmations", () => {
|
|
131
|
+
const validL2 = {
|
|
132
|
+
id: "K-0001",
|
|
133
|
+
layer: "L2",
|
|
134
|
+
created: "2026-08-03",
|
|
135
|
+
lastConfirmedAt: "2026-08-03",
|
|
136
|
+
confirmations: 3,
|
|
137
|
+
status: "active",
|
|
138
|
+
};
|
|
139
|
+
expect(knowledgeEntryMetaSchema.safeParse(validL2).success).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("schema rejects L0 with confirmations", () => {
|
|
143
|
+
const invalidL0 = {
|
|
144
|
+
id: "K-0001",
|
|
145
|
+
layer: "L0",
|
|
146
|
+
created: "2026-08-03",
|
|
147
|
+
confirmations: 5,
|
|
148
|
+
status: "active",
|
|
149
|
+
};
|
|
150
|
+
const result = knowledgeEntryMetaSchema.safeParse(invalidL0);
|
|
151
|
+
expect(result.success).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("schema rejects L2 without confirmations", () => {
|
|
155
|
+
const invalidL2 = {
|
|
156
|
+
id: "K-0001",
|
|
157
|
+
layer: "L2",
|
|
158
|
+
created: "2026-08-03",
|
|
159
|
+
status: "active",
|
|
160
|
+
};
|
|
161
|
+
const result = knowledgeEntryMetaSchema.safeParse(invalidL2);
|
|
162
|
+
expect(result.success).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
});
|