@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,521 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Canonical Compass inventory scanning logic. Moved from @warpgogol/site-kernel
4
+ to @warpgogol/forge for full autonomous mode (RFC-0556). Provides file collection,
5
+ workspace detection, layer classification, risk assessment, and compliance checking
6
+ for Compass source-file inventory.</purpose>
7
+ <non-goals>
8
+ <item>Do not render XML output — that belongs in the compass command handler.</item>
9
+ <item>Do not register commands — this is a pure utility module.</item>
10
+ </non-goals>
11
+ </MODULE_CONTRACT>
12
+ <CHANGE_SUMMARY>
13
+ <item>RFC-0348: collapsed to two-block contract (MODULE_CONTRACT + CHANGE_SUMMARY); removed MODULE_MAP, keywords, responsibilities, COMPASS_BLOCK anchors; coverage modes collapsed to standard/none.</item>
14
+ <item>Post-refactor hardening: exclude src/templates generation inputs from authored Compass requirements.</item>
15
+ <item>Post-refactor hardening: detect nested packages/os workspaces before deriving Compass layer and workspace name.</item>
16
+ <item>RFC-0556: moved canonical implementation from @warpgogol/site-kernel to @warpgogol/forge for autonomous mode.</item>
17
+ </CHANGE_SUMMARY>
18
+ */
19
+
20
+ import { readdir, readFile } from "node:fs/promises";
21
+ import { join, relative, resolve } from "node:path";
22
+ import type { ForgeCommandInput } from "../../../src/types.ts";
23
+ import { hasGeneratedMarker } from "../../../src/utils/generated-marker.ts";
24
+
25
+ const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".astro", ".js", ".jsx", ".mjs", ".mts", ".css"]);
26
+ const IGNORED_DIRECTORY_NAMES = new Set([
27
+ ".git",
28
+ ".turbo",
29
+ ".astro",
30
+ ".wrangler",
31
+ ".vscode",
32
+ ".cache",
33
+ "coverage",
34
+ "dist",
35
+ "node_modules",
36
+ "spec",
37
+ "todo",
38
+ ]);
39
+ const DEFAULT_SCAN_ROOTS = ["apps", "packages", "services"];
40
+ const HIGH_RISK_EXACT_RELATIVE_PATHS = new Set([
41
+ "apps/main/src/content/config.ts",
42
+ "apps/main/src/middleware.ts",
43
+ "packages/os/site-kernel/src/cli/index.ts",
44
+ "packages/os/site-kernel/src/discovery.ts",
45
+ "packages/os/site-kernel/src/registry.ts",
46
+ "packages/os/site-kernel/src/runtime.ts",
47
+ "packages/os/site-kernel/src/types.ts",
48
+ ]);
49
+ const REQUIRED_MARKERS = ["<MODULE_CONTRACT>", "<CHANGE_SUMMARY>"] as const;
50
+ function forbiddenMarkerPattern(tagName: string): RegExp {
51
+ return new RegExp(`<${tagName}\\b`);
52
+ }
53
+
54
+ const FORBIDDEN_PATTERNS: Array<{ name: string; regex: RegExp }> = [
55
+ { name: "MODULE_MAP", regex: forbiddenMarkerPattern("MODULE_MAP") },
56
+ { name: "keywords", regex: forbiddenMarkerPattern("keywords") },
57
+ { name: "responsibilities", regex: forbiddenMarkerPattern("responsibilities") },
58
+ { name: "COMPASS_BLOCK", regex: new RegExp(`</?${"COMPASS_BLOCK"}\\b`) },
59
+ ];
60
+
61
+ type CompassWorkspaceKind = "app" | "package" | "service";
62
+ type CompassRiskClass = "high" | "medium" | "low";
63
+ type CompassComplexity = "non-trivial" | "trivial";
64
+ type CompassScaffoldingMode = "standard" | "none";
65
+ type CompassAuthoringStatus = "authored" | "excluded";
66
+
67
+ export interface CompassInventoryEntry {
68
+ path: string;
69
+ workspaceKind: CompassWorkspaceKind;
70
+ workspaceName: string;
71
+ layer: string;
72
+ extension: string;
73
+ authoringStatus: CompassAuthoringStatus;
74
+ exclusionReason?: string;
75
+ riskClass: CompassRiskClass;
76
+ complexity: CompassComplexity;
77
+ requiredScaffolding: CompassScaffoldingMode;
78
+ nonEmptyLineCount: number;
79
+ hasModuleContract: boolean;
80
+ hasChangeSummary: boolean;
81
+ hasAiInvariant: boolean;
82
+ hasPurpose: boolean;
83
+ hasNonGoals: boolean;
84
+ forbiddenPresent: string[];
85
+ compliant: boolean;
86
+ violations: string[];
87
+ }
88
+
89
+ function getFlagValues(input: ForgeCommandInput, key: string): string[] {
90
+ const value = input.flags[key];
91
+ if (Array.isArray(value)) {
92
+ return value.filter((entry): entry is string => typeof entry === "string");
93
+ }
94
+ return typeof value === "string" ? [value] : [];
95
+ }
96
+
97
+ function resolveScanRoots(workspaceRoot: string, input: ForgeCommandInput): string[] {
98
+ const values = getFlagValues(input, "root");
99
+ const roots = values.length > 0 ? values : DEFAULT_SCAN_ROOTS;
100
+ return roots.map((value) => resolve(workspaceRoot, value));
101
+ }
102
+
103
+ function shouldIgnoreDirectory(name: string): boolean {
104
+ if (IGNORED_DIRECTORY_NAMES.has(name)) {
105
+ return true;
106
+ }
107
+ if (name.startsWith("old-")) {
108
+ return true;
109
+ }
110
+ if (name.startsWith("-")) {
111
+ return true;
112
+ }
113
+ return false;
114
+ }
115
+
116
+ function hasRelevantExtension(filePath: string): boolean {
117
+ for (const extension of SOURCE_EXTENSIONS) {
118
+ if (filePath.endsWith(extension)) {
119
+ return true;
120
+ }
121
+ }
122
+ return false;
123
+ }
124
+
125
+ async function collectSourceFiles(targetPath: string): Promise<string[]> {
126
+ let stat;
127
+ try {
128
+ const { stat: fsStat } = await import("node:fs/promises");
129
+ stat = await fsStat(targetPath);
130
+ } catch {
131
+ return [];
132
+ }
133
+
134
+ if (stat.isFile()) {
135
+ return hasRelevantExtension(targetPath) ? [targetPath] : [];
136
+ }
137
+
138
+ let entries;
139
+ try {
140
+ entries = await readdir(targetPath, { withFileTypes: true });
141
+ } catch {
142
+ return [];
143
+ }
144
+
145
+ const files: string[] = [];
146
+ for (const entry of entries) {
147
+ const absolutePath = join(targetPath, entry.name);
148
+
149
+ if (entry.isDirectory()) {
150
+ if (shouldIgnoreDirectory(entry.name)) {
151
+ continue;
152
+ }
153
+
154
+ const normalizedPath = absolutePath.replace(/\\/g, "/");
155
+ if (
156
+ normalizedPath.endsWith("/src/assets") ||
157
+ normalizedPath.endsWith("/src/icons/gen") ||
158
+ normalizedPath.includes("/public/_video") ||
159
+ normalizedPath.includes("/public/_img")
160
+ ) {
161
+ continue;
162
+ }
163
+
164
+ files.push(...(await collectSourceFiles(absolutePath)));
165
+ continue;
166
+ }
167
+
168
+ if (entry.isFile() && hasRelevantExtension(absolutePath)) {
169
+ files.push(absolutePath);
170
+ }
171
+ }
172
+
173
+ return files;
174
+ }
175
+
176
+ function getRelativeSegments(filePath: string, workspaceRoot: string): string[] {
177
+ return relative(workspaceRoot, filePath).replace(/\\/g, "/").split("/").filter(Boolean);
178
+ }
179
+
180
+ function detectWorkspaceKind(segments: string[]): CompassWorkspaceKind {
181
+ if (segments[0] === "services") return "service";
182
+ return segments[0] === "packages" ? "package" : "app";
183
+ }
184
+
185
+ function detectWorkspaceName(segments: string[]): string {
186
+ if (segments[0] === "packages" && segments[1] === "os") {
187
+ return segments[2] ?? "unknown";
188
+ }
189
+ return segments[1] ?? "unknown";
190
+ }
191
+
192
+ function getWorkspaceRelativeSegments(segments: string[]): string[] {
193
+ if (segments[0] === "packages" && segments[1] === "os") {
194
+ return segments.slice(3);
195
+ }
196
+ return segments.slice(2);
197
+ }
198
+
199
+ function detectLayer(relativePath: string): string {
200
+ if (relativePath.startsWith("bin/")) return "bin";
201
+ if (relativePath === "tools/kernel.config.ts") return "tool-config";
202
+ if (relativePath.startsWith("tools/modules/")) return "tool-module";
203
+ if (relativePath.startsWith("tools/runtime/")) return "tool-runtime";
204
+ if (relativePath.startsWith("src/middleware/")) return "middleware";
205
+ if (relativePath === "src/middleware.ts") return "middleware";
206
+ if (relativePath.startsWith("src/pages/")) return "page";
207
+ if (relativePath.startsWith("src/components/")) return "component";
208
+ if (relativePath.startsWith("src/content/schemas/")) return "schema";
209
+ if (relativePath.startsWith("src/content/")) return "content";
210
+ if (relativePath.startsWith("src/styles/")) return "style";
211
+ if (relativePath.startsWith("src/scripts/")) return "script";
212
+ if (relativePath.startsWith("src/layouts/")) return "layout";
213
+ if (relativePath.startsWith("src/utils/")) return "utility";
214
+ if (relativePath.startsWith("src/configure/")) return "config";
215
+ if (
216
+ relativePath.startsWith("test/") ||
217
+ relativePath.startsWith("src/tests/") ||
218
+ relativePath.endsWith(".test.ts") ||
219
+ relativePath.endsWith(".spec.ts")
220
+ )
221
+ return "test";
222
+ if (relativePath.startsWith("src/")) return "source";
223
+ return "other";
224
+ }
225
+
226
+ function detectRiskClass(pathFromRoot: string, layer: string): CompassRiskClass {
227
+ if (HIGH_RISK_EXACT_RELATIVE_PATHS.has(pathFromRoot)) {
228
+ return "high";
229
+ }
230
+ if (pathFromRoot.includes("/src/scripts/layout-scroll/")) {
231
+ return "high";
232
+ }
233
+ if (pathFromRoot.includes("/src/layouts/")) {
234
+ return "high";
235
+ }
236
+ if (pathFromRoot.includes("/src/middleware/")) {
237
+ return "high";
238
+ }
239
+ if (layer === "tool-runtime" || layer === "tool-config" || layer === "schema") {
240
+ return "medium";
241
+ }
242
+ if (layer === "page" || layer === "component" || layer === "utility" || layer === "script") {
243
+ return "medium";
244
+ }
245
+ return "low";
246
+ }
247
+
248
+ function detectComplexity(source: string): CompassComplexity {
249
+ const nonEmptyLineCount = source.split(/\r?\n/).filter((line) => line.trim().length > 0).length;
250
+ if (nonEmptyLineCount <= 20 && source.length <= 900) {
251
+ return "trivial";
252
+ }
253
+ return "non-trivial";
254
+ }
255
+
256
+ function isSimpleSvgLogoComponent(segments: string[]): boolean {
257
+ return (
258
+ segments.length >= 4 &&
259
+ segments[0] === "apps" &&
260
+ segments[2] === "src" &&
261
+ segments[3] === "components" &&
262
+ segments[4] === "logo"
263
+ );
264
+ }
265
+
266
+ function detectAuthoringStatus(
267
+ segments: string[],
268
+ relativePath: string,
269
+ source: string,
270
+ ): {
271
+ authoringStatus: CompassAuthoringStatus;
272
+ exclusionReason?: string;
273
+ } {
274
+ if (hasGeneratedMarker(source)) {
275
+ return {
276
+ authoringStatus: "excluded",
277
+ exclusionReason: "generated-marker",
278
+ };
279
+ }
280
+
281
+ if (relativePath.startsWith("src/icons/gen/")) {
282
+ return {
283
+ authoringStatus: "excluded",
284
+ exclusionReason: "generated-icon-tree",
285
+ };
286
+ }
287
+
288
+ if (relativePath.startsWith("src/assets/")) {
289
+ return {
290
+ authoringStatus: "excluded",
291
+ exclusionReason: "assets-directory",
292
+ };
293
+ }
294
+
295
+ if (isSimpleSvgLogoComponent(segments)) {
296
+ return {
297
+ authoringStatus: "excluded",
298
+ exclusionReason: "svg-component",
299
+ };
300
+ }
301
+
302
+ if (relativePath.startsWith("dist/")) {
303
+ return {
304
+ authoringStatus: "excluded",
305
+ exclusionReason: "generated-dist",
306
+ };
307
+ }
308
+
309
+ if (/\.generated\.[a-z]+$/i.test(relativePath)) {
310
+ return {
311
+ authoringStatus: "excluded",
312
+ exclusionReason: "generated-file",
313
+ };
314
+ }
315
+
316
+ if (relativePath.startsWith("src/templates/")) {
317
+ return {
318
+ authoringStatus: "excluded",
319
+ exclusionReason: "template-source",
320
+ };
321
+ }
322
+
323
+ if (
324
+ relativePath.startsWith("test/") ||
325
+ relativePath.endsWith(".test.ts") ||
326
+ relativePath.endsWith(".test.js") ||
327
+ relativePath.endsWith(".spec.ts") ||
328
+ relativePath.endsWith(".spec.js")
329
+ ) {
330
+ return {
331
+ authoringStatus: "excluded",
332
+ exclusionReason: "test-file",
333
+ };
334
+ }
335
+
336
+ if (segments.includes("bin")) {
337
+ return {
338
+ authoringStatus: "excluded",
339
+ exclusionReason: "bin-entrypoint",
340
+ };
341
+ }
342
+
343
+ const isRootFile = !relativePath.includes("/");
344
+ if (
345
+ isRootFile &&
346
+ (relativePath.endsWith(".config.mjs") ||
347
+ relativePath.endsWith(".config.cjs") ||
348
+ relativePath.endsWith(".config.js") ||
349
+ relativePath.endsWith(".config.ts"))
350
+ ) {
351
+ return {
352
+ authoringStatus: "excluded",
353
+ exclusionReason: "framework-config",
354
+ };
355
+ }
356
+
357
+ return { authoringStatus: "authored" };
358
+ }
359
+
360
+ function detectRequiredScaffolding(
361
+ authoringStatus: CompassAuthoringStatus,
362
+ ): CompassScaffoldingMode {
363
+ if (authoringStatus === "excluded") {
364
+ return "none";
365
+ }
366
+
367
+ return "standard";
368
+ }
369
+
370
+ function extractBlockContent(source: string, tagName: string): string | null {
371
+ const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
372
+ const match = source.match(new RegExp(`<${escapedTag}>[\\s\\S]*?<\/${escapedTag}>`));
373
+ return match?.[0] ?? null;
374
+ }
375
+
376
+ function countWords(value: string): number {
377
+ return value.trim().split(/\s+/).filter(Boolean).length;
378
+ }
379
+
380
+ function detectComplianceViolations(
381
+ entry: Pick<
382
+ CompassInventoryEntry,
383
+ | "requiredScaffolding"
384
+ | "riskClass"
385
+ | "hasModuleContract"
386
+ | "hasChangeSummary"
387
+ | "hasAiInvariant"
388
+ | "hasPurpose"
389
+ | "hasNonGoals"
390
+ | "forbiddenPresent"
391
+ >,
392
+ ): string[] {
393
+ if (entry.requiredScaffolding === "none") {
394
+ return [];
395
+ }
396
+
397
+ const violations: string[] = [];
398
+
399
+ if (!entry.hasModuleContract) {
400
+ violations.push("missing MODULE_CONTRACT");
401
+ } else {
402
+ if (!entry.hasPurpose) {
403
+ violations.push("purpose missing or < 10 words");
404
+ }
405
+ if (!entry.hasNonGoals) {
406
+ violations.push("no <non-goals> item");
407
+ }
408
+ }
409
+
410
+ if (!entry.hasChangeSummary) {
411
+ violations.push("missing CHANGE_SUMMARY");
412
+ }
413
+
414
+ if (entry.riskClass === "high" && !entry.hasAiInvariant) {
415
+ violations.push("missing @ai-invariant (high-risk)");
416
+ }
417
+
418
+ for (const forbidden of entry.forbiddenPresent) {
419
+ violations.push(`forbidden: ${forbidden} present`);
420
+ }
421
+
422
+ return violations;
423
+ }
424
+
425
+ function detectMarkup(source: string) {
426
+ const hasModuleContract = source.includes(REQUIRED_MARKERS[0]);
427
+ const hasChangeSummary = source.includes(REQUIRED_MARKERS[1]);
428
+ const hasAiInvariant = /@ai-invariant\b/.test(source);
429
+
430
+ let hasPurpose = false;
431
+ let hasNonGoals = false;
432
+
433
+ if (hasModuleContract) {
434
+ const contractBlock = extractBlockContent(source, "MODULE_CONTRACT");
435
+ if (contractBlock) {
436
+ const purposeBlock = extractBlockContent(contractBlock, "purpose");
437
+ if (purposeBlock) {
438
+ const purposeText = purposeBlock.replace(/<[^>]+>/g, " ");
439
+ hasPurpose = countWords(purposeText) >= 10;
440
+ }
441
+
442
+ const nonGoalsBlock = extractBlockContent(contractBlock, "non-goals");
443
+ if (nonGoalsBlock) {
444
+ hasNonGoals = (nonGoalsBlock.match(/<item>/g) ?? []).length >= 1;
445
+ }
446
+ }
447
+ }
448
+
449
+ const forbiddenPresent: string[] = [];
450
+ for (const { name, regex } of FORBIDDEN_PATTERNS) {
451
+ if (regex.test(source)) {
452
+ forbiddenPresent.push(name);
453
+ }
454
+ }
455
+
456
+ return {
457
+ hasModuleContract,
458
+ hasChangeSummary,
459
+ hasAiInvariant,
460
+ hasPurpose,
461
+ hasNonGoals,
462
+ forbiddenPresent,
463
+ };
464
+ }
465
+
466
+ export async function createCompassInventoryEntries(
467
+ workspaceRoot: string,
468
+ input: ForgeCommandInput,
469
+ scanRoot?: string,
470
+ ): Promise<CompassInventoryEntry[]> {
471
+ const roots = scanRoot ? [scanRoot] : resolveScanRoots(workspaceRoot, input);
472
+ const files = (await Promise.all(roots.map((root) => collectSourceFiles(root)))).flat();
473
+ const entries: CompassInventoryEntry[] = [];
474
+
475
+ for (const filePath of files.sort()) {
476
+ const source = await readFile(filePath, "utf8");
477
+ const pathFromRoot = relative(workspaceRoot, filePath).replace(/\\/g, "/");
478
+ const segments = getRelativeSegments(filePath, workspaceRoot);
479
+ const relativePathWithinWorkspace = getWorkspaceRelativeSegments(segments).join("/");
480
+ const layer = detectLayer(relativePathWithinWorkspace);
481
+ const riskClass = detectRiskClass(pathFromRoot, layer);
482
+ const complexity = detectComplexity(source);
483
+ const { authoringStatus, exclusionReason } = detectAuthoringStatus(
484
+ segments,
485
+ relativePathWithinWorkspace,
486
+ source,
487
+ );
488
+ const requiredScaffolding = detectRequiredScaffolding(authoringStatus);
489
+ const markup = detectMarkup(source);
490
+ const nonEmptyLineCount = source.split(/\r?\n/).filter((line) => line.trim().length > 0).length;
491
+ const candidate: CompassInventoryEntry = {
492
+ path: pathFromRoot,
493
+ workspaceKind: detectWorkspaceKind(segments),
494
+ workspaceName: detectWorkspaceName(segments),
495
+ layer,
496
+ extension:
497
+ segments.length > 0
498
+ ? segments[segments.length - 1]!.slice(segments[segments.length - 1]!.lastIndexOf("."))
499
+ : "",
500
+ authoringStatus,
501
+ exclusionReason,
502
+ riskClass,
503
+ complexity,
504
+ requiredScaffolding,
505
+ nonEmptyLineCount,
506
+ hasModuleContract: markup.hasModuleContract,
507
+ hasChangeSummary: markup.hasChangeSummary,
508
+ hasAiInvariant: markup.hasAiInvariant,
509
+ hasPurpose: markup.hasPurpose,
510
+ hasNonGoals: markup.hasNonGoals,
511
+ forbiddenPresent: markup.forbiddenPresent,
512
+ compliant: false,
513
+ violations: [],
514
+ };
515
+ candidate.violations = detectComplianceViolations(candidate);
516
+ candidate.compliant = candidate.violations.length === 0;
517
+ entries.push(candidate);
518
+ }
519
+
520
+ return entries;
521
+ }
@@ -0,0 +1,137 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Git revision lookup by file path. Moved from @warpgogol/site-kernel-integrity
4
+ to @warpgogol/forge for full autonomous mode (RFC-0556). Provides getRevisionByPath
5
+ with integrity-registry lookup and git-history fallback, plus getFileRevisionFromHistory.</purpose>
6
+ <non-goals>
7
+ <item>Do not compute a new per-file counter — reuse the existing integrity revision.</item>
8
+ <item>Do not modify the registry or write any files.</item>
9
+ </non-goals>
10
+ </MODULE_CONTRACT>
11
+ <CHANGE_SUMMARY>
12
+ <item>RFC-0352: initial implementation of getRevisionByPath helper.</item>
13
+ <item>RFC-0556: moved from @warpgogol/site-kernel-integrity to @warpgogol/forge for autonomous mode.</item>
14
+ </CHANGE_SUMMARY>
15
+ */
16
+
17
+ import { execFile } from "node:child_process";
18
+ import { promisify } from "node:util";
19
+ import { createHash } from "node:crypto";
20
+ import { readFile } from "node:fs/promises";
21
+ import { resolve, join } from "node:path";
22
+
23
+ const execFileAsync = promisify(execFile);
24
+ const DEFAULT_GIT_TIMEOUT_MS = 15_000;
25
+
26
+ async function runGit(
27
+ cwd: string,
28
+ args: string[],
29
+ timeoutMs: number = DEFAULT_GIT_TIMEOUT_MS,
30
+ ): Promise<string> {
31
+ const { stdout } = await execFileAsync("git", args, {
32
+ cwd,
33
+ maxBuffer: 20 * 1024 * 1024,
34
+ timeout: timeoutMs,
35
+ });
36
+ return stdout.trimEnd();
37
+ }
38
+
39
+ export async function getFileRevisionFromHistory(cwd: string, repoPath: string): Promise<number> {
40
+ try {
41
+ const output = await runGit(
42
+ cwd,
43
+ ["log", "--follow", "--diff-filter=AMT", "--format=%H", "--", repoPath],
44
+ 15_000,
45
+ ).catch(() => "");
46
+
47
+ if (!output) {
48
+ return 1;
49
+ }
50
+
51
+ const revisions = output.split("\n").filter(Boolean).length;
52
+ return Math.max(1, revisions);
53
+ } catch {
54
+ return 1;
55
+ }
56
+ }
57
+
58
+ interface RegistryEntity {
59
+ currentPath: string;
60
+ firstPath?: string;
61
+ createdAt: string;
62
+ updatedAt: string;
63
+ revision: number;
64
+ contentHash: string;
65
+ gitSha: string;
66
+ status: "active" | "deleted";
67
+ moves?: Array<{ from: string; to: string; detectedAt: string; method: string }>;
68
+ }
69
+
70
+ type EntitiesById = Record<string, RegistryEntity>;
71
+ type PathsCurrent = Record<string, string>;
72
+
73
+ async function loadEntitiesById(cwd: string): Promise<EntitiesById> {
74
+ const filePath = join(cwd, ".integrity", "index", "entities.by-id.json");
75
+ try {
76
+ const content = await readFile(filePath, "utf8");
77
+ return JSON.parse(content) as EntitiesById;
78
+ } catch {
79
+ return {};
80
+ }
81
+ }
82
+
83
+ async function loadPathsCurrent(cwd: string): Promise<PathsCurrent> {
84
+ const filePath = join(cwd, ".integrity", "index", "paths.current.json");
85
+ try {
86
+ const content = await readFile(filePath, "utf8");
87
+ return JSON.parse(content) as PathsCurrent;
88
+ } catch {
89
+ return {};
90
+ }
91
+ }
92
+
93
+ export interface RevisionByPathResult {
94
+ revision: number;
95
+ entityId: string | null;
96
+ contentHash: string;
97
+ }
98
+
99
+ export async function getRevisionByPath(
100
+ cwd: string,
101
+ repoPath: string,
102
+ ): Promise<RevisionByPathResult> {
103
+ const paths = await loadPathsCurrent(cwd);
104
+ const entityId = paths[repoPath];
105
+
106
+ if (entityId) {
107
+ const entities = await loadEntitiesById(cwd);
108
+ const entity = entities[entityId];
109
+ if (entity) {
110
+ let contentHash = entity.contentHash;
111
+ try {
112
+ const abs = resolve(cwd, repoPath);
113
+ const content = await readFile(abs, "utf8");
114
+ contentHash = "sha256-" + createHash("sha256").update(content).digest("hex");
115
+ } catch {
116
+ // keep registry hash
117
+ }
118
+ return {
119
+ revision: entity.revision,
120
+ entityId,
121
+ contentHash,
122
+ };
123
+ }
124
+ }
125
+
126
+ const revision = await getFileRevisionFromHistory(cwd, repoPath);
127
+ let contentHash = "";
128
+ try {
129
+ const abs = resolve(cwd, repoPath);
130
+ const content = await readFile(abs, "utf8");
131
+ contentHash = "sha256-" + createHash("sha256").update(content).digest("hex");
132
+ } catch {
133
+ // file may not exist
134
+ }
135
+
136
+ return { revision, entityId: null, contentHash };
137
+ }