@warpgogol/forge 2.7.0 → 2.8.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.
Files changed (268) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +23 -13
  3. package/README.uk.md +19 -12
  4. package/dist/src/config/forge-config.d.ts +1 -1
  5. package/dist/src/config/forge-config.d.ts.map +1 -1
  6. package/dist/src/config/forge-config.js +2 -2
  7. package/dist/src/config/forge-config.js.map +1 -1
  8. package/dist/src/onboarding/agents-generate.d.ts +2 -1
  9. package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
  10. package/dist/src/onboarding/agents-generate.js +24 -4
  11. package/dist/src/onboarding/agents-generate.js.map +1 -1
  12. package/dist/src/profiles/profile-schema.d.ts +2 -0
  13. package/dist/src/profiles/profile-schema.d.ts.map +1 -1
  14. package/dist/src/profiles/profile-schema.js +1 -0
  15. package/dist/src/profiles/profile-schema.js.map +1 -1
  16. package/dist/src/profiles/stack-profile.d.ts +1 -0
  17. package/dist/src/profiles/stack-profile.d.ts.map +1 -1
  18. package/dist/src/profiles/stack-profile.js +2 -0
  19. package/dist/src/profiles/stack-profile.js.map +1 -1
  20. package/os/adr/adr.module.ts +158 -0
  21. package/os/adr/frontmatter-io.ts +80 -0
  22. package/os/adr/handlers/archive.ts +244 -0
  23. package/os/adr/handlers/implement-stamp.ts +331 -0
  24. package/os/adr/handlers/list-create.ts +207 -0
  25. package/os/adr/handlers/validate.test.ts +232 -0
  26. package/os/adr/handlers/validate.ts +429 -0
  27. package/os/adr/index.ts +33 -0
  28. package/os/adr/types.ts +141 -0
  29. package/os/audit/audit.module.ts +48 -0
  30. package/os/audit/frontmatter-io.ts +84 -0
  31. package/os/audit/handlers/archive.ts +239 -0
  32. package/os/audit/index.ts +23 -0
  33. package/os/audit/types.ts +29 -0
  34. package/os/compass/compass.module.ts +186 -0
  35. package/os/compass/handlers/compass-audit-handler.ts +382 -0
  36. package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
  37. package/os/compass/handlers/compass-inventory-handler.ts +294 -0
  38. package/os/compass/handlers/compass-inventory.ts +521 -0
  39. package/os/compass/handlers/git-revision.ts +137 -0
  40. package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
  41. package/os/compass/handlers/resolve-scan-root.ts +79 -0
  42. package/os/compass/index.ts +33 -0
  43. package/os/core/core.module.ts +948 -0
  44. package/os/core/handlers/assets-check.ts +122 -0
  45. package/os/core/handlers/assets-helpers.ts +174 -0
  46. package/os/core/handlers/assets-list.ts +99 -0
  47. package/os/core/handlers/build.ts +155 -0
  48. package/os/core/handlers/determinism-check.ts +354 -0
  49. package/os/core/handlers/dev.ts +127 -0
  50. package/os/core/handlers/invariant-engine.test.ts +681 -0
  51. package/os/core/handlers/knowledge-compact.ts +248 -0
  52. package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
  53. package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
  54. package/os/core/handlers/note-link-validate.test.ts +117 -0
  55. package/os/core/handlers/note-orphan-detect.test.ts +94 -0
  56. package/os/core/handlers/package-health.test.ts +226 -0
  57. package/os/core/handlers/package-health.ts +231 -0
  58. package/os/core/handlers/pinned-check.ts +200 -0
  59. package/os/core/handlers/pinned-init.ts +333 -0
  60. package/os/core/handlers/pinned-types.ts +50 -0
  61. package/os/core/handlers/pinned-validate.ts +301 -0
  62. package/os/core/handlers/profile-resolve.ts +92 -0
  63. package/os/core/handlers/release-prepare.ts +294 -0
  64. package/os/core/handlers/release-publish.ts +211 -0
  65. package/os/core/handlers/validate.ts +262 -0
  66. package/os/core/handlers/workspace-deps.ts +55 -0
  67. package/os/core/index.ts +13 -0
  68. package/os/exploration/exploration.module.ts +90 -0
  69. package/os/exploration/frontmatter-io.ts +77 -0
  70. package/os/exploration/handlers/archive.ts +159 -0
  71. package/os/exploration/handlers/list.ts +77 -0
  72. package/os/exploration/handlers/show.ts +107 -0
  73. package/os/exploration/index.ts +25 -0
  74. package/os/exploration/types.ts +65 -0
  75. package/os/mission/handlers/archive.test.ts +391 -0
  76. package/os/mission/handlers/archive.ts +453 -0
  77. package/os/mission/index.ts +1 -0
  78. package/os/mission/mission.module.ts +48 -0
  79. package/os/mission/types.ts +41 -0
  80. package/os/naming/index.ts +14 -0
  81. package/os/naming/naming-convention.test.ts +163 -0
  82. package/os/naming/naming-convention.ts +378 -0
  83. package/os/naming/naming.module.ts +36 -0
  84. package/os/notes/index.ts +10 -0
  85. package/os/notes/notes.module.ts +109 -0
  86. package/os/plan/frontmatter-io.ts +82 -0
  87. package/os/plan/handlers/archive.ts +238 -0
  88. package/os/plan/index.ts +23 -0
  89. package/os/plan/plan.module.ts +48 -0
  90. package/os/plan/types.ts +29 -0
  91. package/os/program/discovery.ts +294 -0
  92. package/os/program/handlers/complete.ts +419 -0
  93. package/os/program/handlers/lease.ts +433 -0
  94. package/os/program/handlers/seal.ts +332 -0
  95. package/os/program/handlers/validate.ts +213 -0
  96. package/os/program/lease.ts +156 -0
  97. package/os/program/program.module.ts +212 -0
  98. package/os/program/schemas.ts +218 -0
  99. package/os/program/state.ts +264 -0
  100. package/os/rfc/acceptance.ts +344 -0
  101. package/os/rfc/decision-log.ts +310 -0
  102. package/os/rfc/dna-trace.ts +280 -0
  103. package/os/rfc/frontmatter-io.test.ts +132 -0
  104. package/os/rfc/frontmatter-io.ts +130 -0
  105. package/os/rfc/handlers/archive.ts +300 -0
  106. package/os/rfc/handlers/check.ts +238 -0
  107. package/os/rfc/handlers/implement-stamp.ts +515 -0
  108. package/os/rfc/handlers/index-graph.ts +237 -0
  109. package/os/rfc/handlers/lifecycle.test.ts +127 -0
  110. package/os/rfc/handlers/lifecycle.ts +249 -0
  111. package/os/rfc/handlers/list-create.ts +296 -0
  112. package/os/rfc/handlers/pipeline-status.ts +173 -0
  113. package/os/rfc/handlers/shared.ts +121 -0
  114. package/os/rfc/handlers/supersede-propose.ts +241 -0
  115. package/os/rfc/handlers/validate-rules.test.ts +891 -0
  116. package/os/rfc/handlers/validate-rules.ts +984 -0
  117. package/os/rfc/handlers/validate.ts +166 -0
  118. package/os/rfc/handlers.ts +19 -0
  119. package/os/rfc/index.ts +97 -0
  120. package/os/rfc/rfc.module.ts +416 -0
  121. package/os/rfc/types.ts +596 -0
  122. package/os/rfc/verification-evidence.ts +244 -0
  123. package/os/session/atif-parser.ts +166 -0
  124. package/os/session/frontmatter-io.ts +163 -0
  125. package/os/session/handlers/archive.ts +288 -0
  126. package/os/session/handlers/list.ts +174 -0
  127. package/os/session/handlers/save.ts +348 -0
  128. package/os/session/handlers/validate.ts +250 -0
  129. package/os/session/index.ts +50 -0
  130. package/os/session/session.module.ts +126 -0
  131. package/os/session/types.ts +203 -0
  132. package/os/spec/live-spec-list-show-validate.test.ts +257 -0
  133. package/os/spec/live-spec-list.ts +78 -0
  134. package/os/spec/live-spec-merge.test.ts +261 -0
  135. package/os/spec/live-spec-merge.ts +413 -0
  136. package/os/spec/live-spec-show.ts +117 -0
  137. package/os/spec/live-spec-types.ts +93 -0
  138. package/os/spec/live-spec-validate.ts +170 -0
  139. package/os/spec/spec-materialize.ts +390 -0
  140. package/os/spec/spec-schema.ts +174 -0
  141. package/os/spec/spec-status.ts +284 -0
  142. package/os/spec/spec-validate.ts +513 -0
  143. package/os/spec/spec.module.ts +135 -0
  144. package/os/werkstatt/handlers/lock.ts +168 -0
  145. package/os/werkstatt/handlers/schema.ts +47 -0
  146. package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
  147. package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
  148. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
  149. package/os/werkstatt/index.ts +39 -0
  150. package/os/werkstatt/werkstatt.module.ts +62 -0
  151. package/os/workflow/handlers.ts +304 -0
  152. package/os/workflow/index.ts +28 -0
  153. package/os/workflow/types.ts +99 -0
  154. package/os/workflow/workflow.module.ts +54 -0
  155. package/package.json +3 -3
  156. package/profiles/godot-csharp.yaml +5 -4
  157. package/profiles/knowledge-typescript-turborepo.yaml +110 -0
  158. package/profiles/phaser-turborepo.yaml +5 -5
  159. package/profiles/root-agents-godot.md +133 -0
  160. package/skills/_shared/fo-session-summary.md +132 -16
  161. package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
  162. package/skills/fo/fo-doc-audit/SKILL.md +6 -6
  163. package/skills/fo/fo-handoff/SKILL.md +5 -0
  164. package/skills/fo/fo-idea/SKILL.md +1 -1
  165. package/skills/fo/fo-review/SKILL.md +1 -1
  166. package/skills/fo/fo-session-retro/SKILL.md +8 -0
  167. package/skills/fo/fo-session-save/SKILL.md +8 -0
  168. package/src/cli-output.ts +59 -0
  169. package/src/config/forge-config.ts +524 -0
  170. package/src/forge-module.ts +39 -0
  171. package/src/index.ts +172 -0
  172. package/src/knowledge/budgets.ts +202 -0
  173. package/src/knowledge/compact.ts +405 -0
  174. package/src/knowledge/index.ts +53 -0
  175. package/src/knowledge/parse.ts +227 -0
  176. package/src/knowledge/promote.ts +156 -0
  177. package/src/knowledge/schema.ts +112 -0
  178. package/src/knowledge/serialize.ts +72 -0
  179. package/src/migration-adapters/git-utils.ts +71 -0
  180. package/src/migration-adapters/ignored-files.ts +221 -0
  181. package/src/migration-adapters/index.ts +21 -0
  182. package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
  183. package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
  184. package/src/migration-adapters/registry.ts +61 -0
  185. package/src/migration-adapters/types.ts +68 -0
  186. package/src/onboarding/agents-generate.ts +443 -0
  187. package/src/onboarding/create.ts +427 -0
  188. package/src/onboarding/doctor.ts +1297 -0
  189. package/src/onboarding/init.ts +337 -0
  190. package/src/onboarding/invariant-engine.ts +448 -0
  191. package/src/onboarding/memory-scaffold.ts +189 -0
  192. package/src/onboarding/nested-agents-generate.ts +93 -0
  193. package/src/onboarding/nested-agents-templates.ts +233 -0
  194. package/src/onboarding/profile-validate.ts +139 -0
  195. package/src/onboarding/scaffold-project.ts +299 -0
  196. package/src/onboarding/scaffold.ts +143 -0
  197. package/src/onboarding/upgrade.ts +558 -0
  198. package/src/onboarding/workspace-discovery.ts +167 -0
  199. package/src/profiles/profile-schema.ts +350 -0
  200. package/src/profiles/stack-profile.ts +188 -0
  201. package/src/profiles/terminology-utils.ts +36 -0
  202. package/src/registry.ts +185 -0
  203. package/src/skill-schema.ts +37 -0
  204. package/src/tests/acceptance-criteria.test.ts +99 -0
  205. package/src/tests/adr-implement-stamp.test.ts +368 -0
  206. package/src/tests/agents-generate-domain.test.ts +317 -0
  207. package/src/tests/agents-generate.test.ts +458 -0
  208. package/src/tests/bindings-schema.test.ts +314 -0
  209. package/src/tests/budgets.test.ts +213 -0
  210. package/src/tests/cli-output.test.ts +196 -0
  211. package/src/tests/compact.test.ts +796 -0
  212. package/src/tests/create.test.ts +289 -0
  213. package/src/tests/doctor-autonomy.test.ts +98 -0
  214. package/src/tests/doctor-bindings.test.ts +286 -0
  215. package/src/tests/doctor-domain.test.ts +113 -0
  216. package/src/tests/exploration-handlers.test.ts +256 -0
  217. package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
  218. package/src/tests/forge-config.test.ts +420 -0
  219. package/src/tests/fs-atomic.test.ts +96 -0
  220. package/src/tests/fs.test.ts +103 -0
  221. package/src/tests/generated-marker.test.ts +161 -0
  222. package/src/tests/hash.test.ts +37 -0
  223. package/src/tests/implement-stamp.test.ts +505 -0
  224. package/src/tests/init-bindings.test.ts +183 -0
  225. package/src/tests/knowledge-parse.test.ts +307 -0
  226. package/src/tests/knowledge-pbt.test.ts +164 -0
  227. package/src/tests/memory-scaffold.test.ts +192 -0
  228. package/src/tests/migration-adapters.test.ts +480 -0
  229. package/src/tests/package-files.test.ts +51 -0
  230. package/src/tests/pinned-check.test.ts +194 -0
  231. package/src/tests/pinned-init.test.ts +126 -0
  232. package/src/tests/profile-schema.test.ts +194 -0
  233. package/src/tests/profile-validate.test.ts +120 -0
  234. package/src/tests/program-lease.test.ts +245 -0
  235. package/src/tests/program-paths-extended.test.ts +117 -0
  236. package/src/tests/program-paths.test.ts +98 -0
  237. package/src/tests/program-property.test.ts +139 -0
  238. package/src/tests/program-schemas.test.ts +445 -0
  239. package/src/tests/program-spec-node.test.ts +250 -0
  240. package/src/tests/promote.test.ts +481 -0
  241. package/src/tests/registry.test.ts +73 -0
  242. package/src/tests/scaffold-project.test.ts +150 -0
  243. package/src/tests/session-handlers.test.ts +503 -0
  244. package/src/tests/session-pbt.test.ts +190 -0
  245. package/src/tests/skill-schema.test.ts +146 -0
  246. package/src/tests/skill-validate-knowledge.test.ts +26 -0
  247. package/src/tests/skill-validate.test.ts +254 -0
  248. package/src/tests/stack-profile.test.ts +217 -0
  249. package/src/tests/string-utils.test.ts +42 -0
  250. package/src/tests/upgrade.test.ts +428 -0
  251. package/src/tests/werkstatt-lock.test.ts +343 -0
  252. package/src/tests/workspace-discovery-domain.test.ts +112 -0
  253. package/src/tests/workspace-discovery.test.ts +135 -0
  254. package/src/types.ts +221 -0
  255. package/src/utils/fs-atomic.ts +93 -0
  256. package/src/utils/fs-idempotent.ts +41 -0
  257. package/src/utils/fs-trash-sync.ts +37 -0
  258. package/src/utils/fs-trash.ts +24 -0
  259. package/src/utils/fs.ts +74 -0
  260. package/src/utils/generated-marker.ts +166 -0
  261. package/src/utils/hash.ts +19 -0
  262. package/src/utils/index.ts +29 -0
  263. package/src/utils/string-utils.ts +19 -0
  264. package/src/validators/note-frontmatter-validate.ts +142 -0
  265. package/src/validators/note-link-validate.ts +145 -0
  266. package/src/validators/note-orphan-detect.ts +146 -0
  267. package/src/validators/port-validate.ts +97 -0
  268. package/src/validators/skill-validate.ts +831 -0
@@ -0,0 +1,29 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Barrel export for forge utilities — canonical, autonomous, no @warpgogol/* dependencies.</purpose>
4
+ <non-goals>
5
+ <item>Do not add non-utility exports here — use the appropriate forge module.</item>
6
+ <item>Do not re-export from @warpgogol/* packages — this package must remain dependency-free.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>Initial utility barrel: fs-atomic, generated-marker, string-utils, fs, hash.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ export { writeFileAtomic, type WriteFileAtomicOptions } from "./fs-atomic.ts";
15
+ export { writeFileIfChanged } from "./fs-idempotent.ts";
16
+ export {
17
+ GENERATED_MARKER,
18
+ hasGeneratedMarker,
19
+ stripGeneratedMarker,
20
+ buildGeneratedHeader,
21
+ isGeneratedMarkerTextCandidate,
22
+ type GeneratedHeaderInput,
23
+ type StripGeneratedMarkerResult,
24
+ } from "./generated-marker.ts";
25
+ export { toKebabCase } from "./string-utils.ts";
26
+ export { collectFiles, fileExists, type CollectFilesOptions } from "./fs.ts";
27
+ export { byteHash } from "./hash.ts";
28
+ export { trashPath } from "./fs-trash.ts";
29
+ export { trashSync } from "./fs-trash-sync.ts";
@@ -0,0 +1,19 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Trivial string utilities — inlined from @warpgogol/share to avoid dependency.</purpose>
4
+ <non-goals>
5
+ <item>Do not add non-string utilities here — use dedicated utility modules.</item>
6
+ <item>Do not introduce @warpgogol/* imports — this package must remain dependency-free.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>Initial inline of toKebabCase from @warpgogol/werkstatt-shared/share/string-utils.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ export function toKebabCase(value: string): string {
15
+ return value
16
+ .toLowerCase()
17
+ .replace(/[^a-z0-9]+/g, "-")
18
+ .replace(/^-+|-+$/g, "");
19
+ }
@@ -0,0 +1,142 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Validate frontmatter consistency across a markdown note vault — checks for required fields in YAML frontmatter.</purpose>
4
+ <non-goals>
5
+ <item>Do not import from @warpgogol/* — this module is portable.</item>
6
+ <item>Do not implement domain-specific logic beyond frontmatter validation.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>RFC-0808: initial note.frontmatter.validate command handler.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import { parse as parseYaml } from "yaml";
17
+ import type { ForgeCommandResult } from "../types.ts";
18
+
19
+ interface NoteFrontmatterValidateInput {
20
+ flags: Record<string, unknown>;
21
+ }
22
+
23
+ export interface FrontmatterViolation {
24
+ file: string;
25
+ field: string;
26
+ rule: "NOTE-02";
27
+ message: string;
28
+ }
29
+
30
+ export interface NoteFrontmatterValidateResult {
31
+ command: "note.frontmatter.validate";
32
+ violations: FrontmatterViolation[];
33
+ count: number;
34
+ }
35
+
36
+ function collectMarkdownFiles(dir: string, baseDir: string, results: string[]): void {
37
+ const SKIP_DIRS = new Set(["node_modules", ".git", ".turbo", "dist", ".cache", ".obsidian"]);
38
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
39
+ for (const entry of entries) {
40
+ if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue;
41
+ const fullPath = path.join(dir, entry.name);
42
+ const relPath = path.relative(baseDir, fullPath).replace(/\\/g, "/");
43
+ if (entry.isDirectory()) {
44
+ collectMarkdownFiles(fullPath, baseDir, results);
45
+ } else if (entry.name.endsWith(".md")) {
46
+ results.push(relPath);
47
+ }
48
+ }
49
+ }
50
+
51
+ export function runNoteFrontmatterValidate(
52
+ input: NoteFrontmatterValidateInput,
53
+ context: { workspaceRoot?: string },
54
+ ): ForgeCommandResult<NoteFrontmatterValidateResult> {
55
+ const workspaceRoot = context.workspaceRoot ?? process.cwd();
56
+ const vaultDir = (input.flags["vault-dir"] as string) ?? "vault";
57
+ const requiredFields = ((input.flags["fields"] as string) ?? "title")
58
+ .split(",")
59
+ .map((f) => f.trim())
60
+ .filter(Boolean);
61
+
62
+ const scanDir = path.join(workspaceRoot, vaultDir);
63
+
64
+ if (!fs.existsSync(scanDir)) {
65
+ return {
66
+ data: {
67
+ command: "note.frontmatter.validate",
68
+ violations: [],
69
+ count: 0,
70
+ },
71
+ exitCode: 0,
72
+ summary: "note.frontmatter.validate: vault directory not found — 0 violations",
73
+ };
74
+ }
75
+
76
+ const allMdFiles: string[] = [];
77
+ collectMarkdownFiles(scanDir, workspaceRoot, allMdFiles);
78
+
79
+ const violations: FrontmatterViolation[] = [];
80
+
81
+ for (const file of allMdFiles) {
82
+ const fullPath = path.join(workspaceRoot, file);
83
+ try {
84
+ const content = fs.readFileSync(fullPath, "utf8");
85
+ const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
86
+ if (!fmMatch) {
87
+ for (const field of requiredFields) {
88
+ violations.push({
89
+ file,
90
+ field,
91
+ rule: "NOTE-02",
92
+ message: `File '${file}' has no frontmatter — missing field '${field}'`,
93
+ });
94
+ }
95
+ continue;
96
+ }
97
+ let fm: Record<string, unknown> = {};
98
+ try {
99
+ fm = parseYaml(fmMatch[1]) as Record<string, unknown>;
100
+ } catch {
101
+ for (const field of requiredFields) {
102
+ violations.push({
103
+ file,
104
+ field,
105
+ rule: "NOTE-02",
106
+ message: `File '${file}' has malformed frontmatter — missing field '${field}'`,
107
+ });
108
+ }
109
+ continue;
110
+ }
111
+ for (const field of requiredFields) {
112
+ if (!(field in fm) || fm[field] == null || fm[field] === "") {
113
+ if (field === "title") {
114
+ const hasH1 = /^\s*#\s+\S/.test(content.slice(fmMatch[0].length));
115
+ if (hasH1) continue;
116
+ }
117
+ violations.push({
118
+ file,
119
+ field,
120
+ rule: "NOTE-02",
121
+ message: `File '${file}' missing required field '${field}'`,
122
+ });
123
+ }
124
+ }
125
+ } catch {
126
+ // skip unreadable files
127
+ }
128
+ }
129
+
130
+ return {
131
+ data: {
132
+ command: "note.frontmatter.validate",
133
+ violations,
134
+ count: violations.length,
135
+ },
136
+ exitCode: violations.length > 0 ? 1 : 0,
137
+ summary:
138
+ violations.length > 0
139
+ ? `note.frontmatter.validate: ${violations.length} violation(s) found`
140
+ : "note.frontmatter.validate: OK — 0 violations",
141
+ };
142
+ }
@@ -0,0 +1,145 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Validate wikilink integrity across a markdown note vault — scans [[wikilinks]] and resolves each against the note graph.</purpose>
4
+ <non-goals>
5
+ <item>Do not import from @warpgogol/* — this module is portable.</item>
6
+ <item>Do not implement domain-specific logic beyond wikilink resolution.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>RFC-0808: initial note.link.validate command handler.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import { parse as parseYaml } from "yaml";
17
+ import type { ForgeCommandResult } from "../types.ts";
18
+
19
+ interface NoteLinkValidateInput {
20
+ flags: Record<string, unknown>;
21
+ }
22
+
23
+ export interface NoteLinkViolation {
24
+ file: string;
25
+ line: number;
26
+ link: string;
27
+ rule: "NOTE-01";
28
+ message: string;
29
+ }
30
+
31
+ export interface NoteLinkValidateResult {
32
+ command: "note.link.validate";
33
+ violations: NoteLinkViolation[];
34
+ count: number;
35
+ }
36
+
37
+ function collectMarkdownFiles(dir: string, baseDir: string, results: string[]): void {
38
+ const SKIP_DIRS = new Set(["node_modules", ".git", ".turbo", "dist", ".cache", ".obsidian"]);
39
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
40
+ for (const entry of entries) {
41
+ if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue;
42
+ const fullPath = path.join(dir, entry.name);
43
+ const relPath = path.relative(baseDir, fullPath).replace(/\\/g, "/");
44
+ if (entry.isDirectory()) {
45
+ collectMarkdownFiles(fullPath, baseDir, results);
46
+ } else if (entry.name.endsWith(".md")) {
47
+ results.push(relPath);
48
+ }
49
+ }
50
+ }
51
+
52
+ export function runNoteLinkValidate(
53
+ input: NoteLinkValidateInput,
54
+ context: { workspaceRoot?: string },
55
+ ): ForgeCommandResult<NoteLinkValidateResult> {
56
+ const workspaceRoot = context.workspaceRoot ?? process.cwd();
57
+ const vaultDir = (input.flags["vault-dir"] as string) ?? "vault";
58
+ const pathFilter = input.flags["path"] as string | undefined;
59
+ const scanDir = pathFilter
60
+ ? path.join(workspaceRoot, vaultDir, pathFilter)
61
+ : path.join(workspaceRoot, vaultDir);
62
+
63
+ if (!fs.existsSync(scanDir)) {
64
+ return {
65
+ data: {
66
+ command: "note.link.validate",
67
+ violations: [],
68
+ count: 0,
69
+ },
70
+ exitCode: 0,
71
+ summary: "note.link.validate: vault directory not found — 0 violations",
72
+ };
73
+ }
74
+
75
+ const allMdFiles: string[] = [];
76
+ collectMarkdownFiles(scanDir, workspaceRoot, allMdFiles);
77
+
78
+ const noteMap = new Map<string, string>();
79
+ for (const mdFile of allMdFiles) {
80
+ const basename = path.basename(mdFile, ".md");
81
+ noteMap.set(basename, mdFile);
82
+ const fullPath = path.join(workspaceRoot, mdFile);
83
+ try {
84
+ const content = fs.readFileSync(fullPath, "utf8");
85
+ const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
86
+ if (fmMatch) {
87
+ try {
88
+ const fm = parseYaml(fmMatch[1]) as Record<string, unknown>;
89
+ if (fm.aliases && Array.isArray(fm.aliases)) {
90
+ for (const alias of fm.aliases) {
91
+ if (typeof alias === "string") noteMap.set(alias, mdFile);
92
+ }
93
+ }
94
+ } catch {
95
+ // skip malformed frontmatter
96
+ }
97
+ }
98
+ } catch {
99
+ // skip unreadable files
100
+ }
101
+ }
102
+
103
+ const linkRegex = /\[\[([^\]]+?)\]\]/g;
104
+ const violations: NoteLinkViolation[] = [];
105
+
106
+ for (const file of allMdFiles) {
107
+ const fullPath = path.join(workspaceRoot, file);
108
+ try {
109
+ const content = fs.readFileSync(fullPath, "utf8");
110
+ const lines = content.split(/\r?\n/);
111
+ for (let i = 0; i < lines.length; i++) {
112
+ let match: RegExpExecArray | null;
113
+ linkRegex.lastIndex = 0;
114
+ while ((match = linkRegex.exec(lines[i])) !== null) {
115
+ const linkTarget = match[1].split("|")[0].split("#")[0].trim();
116
+ if (!linkTarget) continue;
117
+ if (!noteMap.has(linkTarget)) {
118
+ violations.push({
119
+ file,
120
+ line: i + 1,
121
+ link: match[0],
122
+ rule: "NOTE-01",
123
+ message: `Wikilink target '${linkTarget}' not found in vault`,
124
+ });
125
+ }
126
+ }
127
+ }
128
+ } catch {
129
+ // skip unreadable files
130
+ }
131
+ }
132
+
133
+ return {
134
+ data: {
135
+ command: "note.link.validate",
136
+ violations,
137
+ count: violations.length,
138
+ },
139
+ exitCode: violations.length > 0 ? 1 : 0,
140
+ summary:
141
+ violations.length > 0
142
+ ? `note.link.validate: ${violations.length} violation(s) found`
143
+ : "note.link.validate: OK — 0 violations",
144
+ };
145
+ }
@@ -0,0 +1,146 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Detect orphan notes in a markdown note vault — notes with zero inbound wikilinks.</purpose>
4
+ <non-goals>
5
+ <item>Do not import from @warpgogol/* — this module is portable.</item>
6
+ <item>Do not implement domain-specific logic beyond orphan detection.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>RFC-0808: initial note.orphan.detect command handler.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import { parse as parseYaml } from "yaml";
17
+ import type { ForgeCommandResult } from "../types.ts";
18
+
19
+ interface NoteOrphanDetectInput {
20
+ flags: Record<string, unknown>;
21
+ }
22
+
23
+ export interface OrphanReport {
24
+ file: string;
25
+ inboundLinks: 0;
26
+ severity: "warning";
27
+ }
28
+
29
+ export interface NoteOrphanDetectResult {
30
+ command: "note.orphan.detect";
31
+ orphans: OrphanReport[];
32
+ count: number;
33
+ }
34
+
35
+ function collectMarkdownFiles(dir: string, baseDir: string, results: string[]): void {
36
+ const SKIP_DIRS = new Set(["node_modules", ".git", ".turbo", "dist", ".cache", ".obsidian"]);
37
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
38
+ for (const entry of entries) {
39
+ if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue;
40
+ const fullPath = path.join(dir, entry.name);
41
+ const relPath = path.relative(baseDir, fullPath).replace(/\\/g, "/");
42
+ if (entry.isDirectory()) {
43
+ collectMarkdownFiles(fullPath, baseDir, results);
44
+ } else if (entry.name.endsWith(".md")) {
45
+ results.push(relPath);
46
+ }
47
+ }
48
+ }
49
+
50
+ export function runNoteOrphanDetect(
51
+ input: NoteOrphanDetectInput,
52
+ context: { workspaceRoot?: string },
53
+ ): ForgeCommandResult<NoteOrphanDetectResult> {
54
+ const workspaceRoot = context.workspaceRoot ?? process.cwd();
55
+ const vaultDir = (input.flags["vault-dir"] as string) ?? "vault";
56
+ const scanDir = path.join(workspaceRoot, vaultDir);
57
+
58
+ if (!fs.existsSync(scanDir)) {
59
+ return {
60
+ data: {
61
+ command: "note.orphan.detect",
62
+ orphans: [],
63
+ count: 0,
64
+ },
65
+ exitCode: 0,
66
+ summary: "note.orphan.detect: vault directory not found — 0 orphans",
67
+ };
68
+ }
69
+
70
+ const allMdFiles: string[] = [];
71
+ collectMarkdownFiles(scanDir, workspaceRoot, allMdFiles);
72
+
73
+ const noteMap = new Map<string, string>();
74
+ for (const mdFile of allMdFiles) {
75
+ const basename = path.basename(mdFile, ".md");
76
+ noteMap.set(basename, mdFile);
77
+ const fullPath = path.join(workspaceRoot, mdFile);
78
+ try {
79
+ const content = fs.readFileSync(fullPath, "utf8");
80
+ const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
81
+ if (fmMatch) {
82
+ try {
83
+ const fm = parseYaml(fmMatch[1]) as Record<string, unknown>;
84
+ if (fm.aliases && Array.isArray(fm.aliases)) {
85
+ for (const alias of fm.aliases) {
86
+ if (typeof alias === "string") noteMap.set(alias, mdFile);
87
+ }
88
+ }
89
+ } catch {
90
+ // skip malformed frontmatter
91
+ }
92
+ }
93
+ } catch {
94
+ // skip unreadable files
95
+ }
96
+ }
97
+
98
+ const inboundLinks = new Map<string, number>();
99
+ for (const mdFile of allMdFiles) {
100
+ inboundLinks.set(mdFile, 0);
101
+ }
102
+
103
+ const linkRegex = /\[\[([^\]]+?)\]\]/g;
104
+ for (const file of allMdFiles) {
105
+ const fullPath = path.join(workspaceRoot, file);
106
+ try {
107
+ const content = fs.readFileSync(fullPath, "utf8");
108
+ let match: RegExpExecArray | null;
109
+ linkRegex.lastIndex = 0;
110
+ while ((match = linkRegex.exec(content)) !== null) {
111
+ const linkTarget = match[1].split("|")[0].split("#")[0].trim();
112
+ if (!linkTarget) continue;
113
+ const targetFile = noteMap.get(linkTarget);
114
+ if (targetFile && targetFile !== file) {
115
+ inboundLinks.set(targetFile, (inboundLinks.get(targetFile) ?? 0) + 1);
116
+ }
117
+ }
118
+ } catch {
119
+ // skip unreadable files
120
+ }
121
+ }
122
+
123
+ const orphans: OrphanReport[] = [];
124
+ for (const [file, count] of inboundLinks) {
125
+ if (count === 0) {
126
+ orphans.push({
127
+ file,
128
+ inboundLinks: 0,
129
+ severity: "warning",
130
+ });
131
+ }
132
+ }
133
+
134
+ return {
135
+ data: {
136
+ command: "note.orphan.detect",
137
+ orphans,
138
+ count: orphans.length,
139
+ },
140
+ exitCode: 0,
141
+ summary:
142
+ orphans.length > 0
143
+ ? `note.orphan.detect: ${orphans.length} orphan(s) found`
144
+ : "note.orphan.detect: OK — 0 orphans",
145
+ };
146
+ }
@@ -0,0 +1,97 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>forge.port.validate — validates that a ported skill or command complies with forge contracts (frontmatter, registry, no project-specific dependencies).</purpose>
4
+ <non-goals>
5
+ <item>Do not validate all skills — only the named skill or command.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0374: initial forge.port.validate handler.</item>
10
+ </CHANGE_SUMMARY>
11
+ */
12
+
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import { FORGE_SKILLS } from "../registry.ts";
16
+
17
+ interface PortValidateResult {
18
+ command: string;
19
+ status: "pass" | "fail";
20
+ violations: string[];
21
+ }
22
+
23
+ // RFC-0556: removed @warpgogol/site-kernel-checks and @warpgogol/site-kernel-handoff
24
+ // from this list — os/compass and os/werkstatt no longer import them (handlers inlined).
25
+ // RFC-0776: @warpgogol/site-kernel replaced by @warpgogol/werkstatt/kernel.
26
+ // @warpgogol/werkstatt/kernel remains forbidden for src/ portability.
27
+ const FORBIDDEN_IMPORTS = [
28
+ "@warpgogol/werkstatt/kernel",
29
+ "@warpgogol/werkstatt-site/ui",
30
+ "@warpgogol/werkstatt-shared/share/page",
31
+ ];
32
+
33
+ export function runPortValidate(
34
+ input: { flags: { name?: string } },
35
+ context: unknown,
36
+ ): PortValidateResult {
37
+ const ctx = context as { workspaceRoot?: string };
38
+ const workspaceRoot = ctx?.workspaceRoot ?? process.cwd();
39
+ const forgeRoot = path.join(workspaceRoot, "packages", "forge");
40
+ const name = input.flags?.name;
41
+
42
+ if (!name) {
43
+ return {
44
+ command: "forge.port.validate",
45
+ status: "fail",
46
+ violations: ["--name flag is required"],
47
+ };
48
+ }
49
+
50
+ const violations: string[] = [];
51
+
52
+ // Check if skill exists in registry
53
+ const skillEntry = FORGE_SKILLS.find((s) => s.name === name);
54
+ if (!skillEntry) {
55
+ violations.push(`Skill "${name}" not found in forge registry`);
56
+ }
57
+
58
+ // Check for forbidden imports in TypeScript files under the skill/command directory
59
+ const skillDir = path.join(forgeRoot, "skills");
60
+ const searchDirs = [
61
+ path.join(skillDir, "fo", name),
62
+ path.join(skillDir, "shared", name),
63
+ path.join(skillDir, "meta", name),
64
+ path.join(forgeRoot, "os"),
65
+ ];
66
+
67
+ for (const dir of searchDirs) {
68
+ if (fs.existsSync(dir)) {
69
+ scanForForbiddenImports(dir, FORBIDDEN_IMPORTS, violations);
70
+ }
71
+ }
72
+
73
+ return {
74
+ command: "forge.port.validate",
75
+ status: violations.length === 0 ? "pass" : "fail",
76
+ violations,
77
+ };
78
+ }
79
+
80
+ function scanForForbiddenImports(dir: string, forbidden: string[], violations: string[]): void {
81
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
82
+ for (const entry of entries) {
83
+ const fullPath = path.join(dir, entry.name);
84
+ if (entry.isDirectory()) {
85
+ scanForForbiddenImports(fullPath, forbidden, violations);
86
+ } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".mjs")) {
87
+ const content = fs.readFileSync(fullPath, "utf8");
88
+ for (const imp of forbidden) {
89
+ if (content.includes(imp)) {
90
+ violations.push(
91
+ `Forbidden import "${imp}" found in ${path.relative(process.cwd(), fullPath)}`,
92
+ );
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }