gcf-aif-ext 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -0
  3. package/commands/gcf.js +173 -0
  4. package/extension.json +24 -0
  5. package/hooks/pre-commit.js +91 -0
  6. package/injections/commit-status.md +6 -0
  7. package/injections/plan-context.md +8 -0
  8. package/injections/review-pillars.md +7 -0
  9. package/lib/anchors.js +109 -0
  10. package/lib/audit-core.js +102 -0
  11. package/lib/context.js +77 -0
  12. package/lib/frontmatter.js +26 -0
  13. package/lib/gcf-paths.js +60 -0
  14. package/lib/git.js +19 -0
  15. package/lib/id.js +42 -0
  16. package/lib/index-gen.js +51 -0
  17. package/lib/linter.js +61 -0
  18. package/lib/playtest-list.js +13 -0
  19. package/lib/safe-path.js +71 -0
  20. package/lib/transitions.js +30 -0
  21. package/lib/validate-card.js +48 -0
  22. package/lib/verdict-fs.js +126 -0
  23. package/node_modules/cnlp-kit/CHANGELOG.md +625 -0
  24. package/node_modules/cnlp-kit/LICENSE +21 -0
  25. package/node_modules/cnlp-kit/README.md +242 -0
  26. package/node_modules/cnlp-kit/bin/cnlp.js +334 -0
  27. package/node_modules/cnlp-kit/cnlp-format.md +470 -0
  28. package/node_modules/cnlp-kit/package.json +40 -0
  29. package/node_modules/cnlp-kit/profiles/adr.md +129 -0
  30. package/node_modules/cnlp-kit/profiles/api.md +87 -0
  31. package/node_modules/cnlp-kit/profiles/concept.md +70 -0
  32. package/node_modules/cnlp-kit/profiles/docset.md +103 -0
  33. package/node_modules/cnlp-kit/profiles/guide.md +51 -0
  34. package/node_modules/cnlp-kit/profiles/profile.md +69 -0
  35. package/node_modules/cnlp-kit/profiles/reference.md +61 -0
  36. package/node_modules/cnlp-kit/profiles/skill.md +75 -0
  37. package/node_modules/cnlp-kit/profiles/task.md +65 -0
  38. package/node_modules/cnlp-kit/quality-rules.md +69 -0
  39. package/node_modules/cnlp-kit/skills/cnlp-docset/SKILL.md +73 -0
  40. package/node_modules/cnlp-kit/skills/cnlp-migrate/SKILL.md +64 -0
  41. package/node_modules/cnlp-kit/src/cnlp.js +534 -0
  42. package/node_modules/cnlp-kit/src/config.js +352 -0
  43. package/node_modules/yaml/LICENSE +13 -0
  44. package/node_modules/yaml/README.md +172 -0
  45. package/node_modules/yaml/bin.mjs +11 -0
  46. package/node_modules/yaml/browser/dist/compose/compose-collection.js +88 -0
  47. package/node_modules/yaml/browser/dist/compose/compose-doc.js +43 -0
  48. package/node_modules/yaml/browser/dist/compose/compose-node.js +109 -0
  49. package/node_modules/yaml/browser/dist/compose/compose-scalar.js +86 -0
  50. package/node_modules/yaml/browser/dist/compose/composer.js +219 -0
  51. package/node_modules/yaml/browser/dist/compose/resolve-block-map.js +115 -0
  52. package/node_modules/yaml/browser/dist/compose/resolve-block-scalar.js +198 -0
  53. package/node_modules/yaml/browser/dist/compose/resolve-block-seq.js +49 -0
  54. package/node_modules/yaml/browser/dist/compose/resolve-end.js +37 -0
  55. package/node_modules/yaml/browser/dist/compose/resolve-flow-collection.js +207 -0
  56. package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +226 -0
  57. package/node_modules/yaml/browser/dist/compose/resolve-props.js +146 -0
  58. package/node_modules/yaml/browser/dist/compose/util-contains-newline.js +34 -0
  59. package/node_modules/yaml/browser/dist/compose/util-empty-scalar-position.js +26 -0
  60. package/node_modules/yaml/browser/dist/compose/util-flow-indent-check.js +15 -0
  61. package/node_modules/yaml/browser/dist/compose/util-map-includes.js +13 -0
  62. package/node_modules/yaml/browser/dist/doc/Document.js +335 -0
  63. package/node_modules/yaml/browser/dist/doc/anchors.js +71 -0
  64. package/node_modules/yaml/browser/dist/doc/applyReviver.js +55 -0
  65. package/node_modules/yaml/browser/dist/doc/createNode.js +88 -0
  66. package/node_modules/yaml/browser/dist/doc/directives.js +176 -0
  67. package/node_modules/yaml/browser/dist/errors.js +57 -0
  68. package/node_modules/yaml/browser/dist/index.js +17 -0
  69. package/node_modules/yaml/browser/dist/log.js +11 -0
  70. package/node_modules/yaml/browser/dist/nodes/Alias.js +118 -0
  71. package/node_modules/yaml/browser/dist/nodes/Collection.js +147 -0
  72. package/node_modules/yaml/browser/dist/nodes/Node.js +38 -0
  73. package/node_modules/yaml/browser/dist/nodes/Pair.js +36 -0
  74. package/node_modules/yaml/browser/dist/nodes/Scalar.js +24 -0
  75. package/node_modules/yaml/browser/dist/nodes/YAMLMap.js +144 -0
  76. package/node_modules/yaml/browser/dist/nodes/YAMLSeq.js +113 -0
  77. package/node_modules/yaml/browser/dist/nodes/addPairToJSMap.js +63 -0
  78. package/node_modules/yaml/browser/dist/nodes/identity.js +36 -0
  79. package/node_modules/yaml/browser/dist/nodes/toJS.js +37 -0
  80. package/node_modules/yaml/browser/dist/parse/cst-scalar.js +214 -0
  81. package/node_modules/yaml/browser/dist/parse/cst-stringify.js +61 -0
  82. package/node_modules/yaml/browser/dist/parse/cst-visit.js +97 -0
  83. package/node_modules/yaml/browser/dist/parse/cst.js +98 -0
  84. package/node_modules/yaml/browser/dist/parse/lexer.js +721 -0
  85. package/node_modules/yaml/browser/dist/parse/line-counter.js +39 -0
  86. package/node_modules/yaml/browser/dist/parse/parser.js +975 -0
  87. package/node_modules/yaml/browser/dist/public-api.js +102 -0
  88. package/node_modules/yaml/browser/dist/schema/Schema.js +37 -0
  89. package/node_modules/yaml/browser/dist/schema/common/map.js +17 -0
  90. package/node_modules/yaml/browser/dist/schema/common/null.js +15 -0
  91. package/node_modules/yaml/browser/dist/schema/common/seq.js +17 -0
  92. package/node_modules/yaml/browser/dist/schema/common/string.js +14 -0
  93. package/node_modules/yaml/browser/dist/schema/core/bool.js +19 -0
  94. package/node_modules/yaml/browser/dist/schema/core/float.js +43 -0
  95. package/node_modules/yaml/browser/dist/schema/core/int.js +38 -0
  96. package/node_modules/yaml/browser/dist/schema/core/schema.js +23 -0
  97. package/node_modules/yaml/browser/dist/schema/json/schema.js +62 -0
  98. package/node_modules/yaml/browser/dist/schema/tags.js +96 -0
  99. package/node_modules/yaml/browser/dist/schema/yaml-1.1/binary.js +58 -0
  100. package/node_modules/yaml/browser/dist/schema/yaml-1.1/bool.js +26 -0
  101. package/node_modules/yaml/browser/dist/schema/yaml-1.1/float.js +46 -0
  102. package/node_modules/yaml/browser/dist/schema/yaml-1.1/int.js +71 -0
  103. package/node_modules/yaml/browser/dist/schema/yaml-1.1/merge.js +67 -0
  104. package/node_modules/yaml/browser/dist/schema/yaml-1.1/omap.js +74 -0
  105. package/node_modules/yaml/browser/dist/schema/yaml-1.1/pairs.js +78 -0
  106. package/node_modules/yaml/browser/dist/schema/yaml-1.1/schema.js +39 -0
  107. package/node_modules/yaml/browser/dist/schema/yaml-1.1/set.js +93 -0
  108. package/node_modules/yaml/browser/dist/schema/yaml-1.1/timestamp.js +101 -0
  109. package/node_modules/yaml/browser/dist/stringify/foldFlowLines.js +146 -0
  110. package/node_modules/yaml/browser/dist/stringify/stringify.js +129 -0
  111. package/node_modules/yaml/browser/dist/stringify/stringifyCollection.js +153 -0
  112. package/node_modules/yaml/browser/dist/stringify/stringifyComment.js +20 -0
  113. package/node_modules/yaml/browser/dist/stringify/stringifyDocument.js +85 -0
  114. package/node_modules/yaml/browser/dist/stringify/stringifyNumber.js +25 -0
  115. package/node_modules/yaml/browser/dist/stringify/stringifyPair.js +150 -0
  116. package/node_modules/yaml/browser/dist/stringify/stringifyString.js +336 -0
  117. package/node_modules/yaml/browser/dist/util.js +11 -0
  118. package/node_modules/yaml/browser/dist/visit.js +233 -0
  119. package/node_modules/yaml/browser/index.js +5 -0
  120. package/node_modules/yaml/browser/package.json +3 -0
  121. package/node_modules/yaml/dist/cli.d.ts +8 -0
  122. package/node_modules/yaml/dist/cli.mjs +201 -0
  123. package/node_modules/yaml/dist/compose/compose-collection.d.ts +11 -0
  124. package/node_modules/yaml/dist/compose/compose-collection.js +90 -0
  125. package/node_modules/yaml/dist/compose/compose-doc.d.ts +7 -0
  126. package/node_modules/yaml/dist/compose/compose-doc.js +45 -0
  127. package/node_modules/yaml/dist/compose/compose-node.d.ts +29 -0
  128. package/node_modules/yaml/dist/compose/compose-node.js +112 -0
  129. package/node_modules/yaml/dist/compose/compose-scalar.d.ts +5 -0
  130. package/node_modules/yaml/dist/compose/compose-scalar.js +88 -0
  131. package/node_modules/yaml/dist/compose/composer.d.ts +63 -0
  132. package/node_modules/yaml/dist/compose/composer.js +224 -0
  133. package/node_modules/yaml/dist/compose/resolve-block-map.d.ts +6 -0
  134. package/node_modules/yaml/dist/compose/resolve-block-map.js +117 -0
  135. package/node_modules/yaml/dist/compose/resolve-block-scalar.d.ts +11 -0
  136. package/node_modules/yaml/dist/compose/resolve-block-scalar.js +200 -0
  137. package/node_modules/yaml/dist/compose/resolve-block-seq.d.ts +6 -0
  138. package/node_modules/yaml/dist/compose/resolve-block-seq.js +51 -0
  139. package/node_modules/yaml/dist/compose/resolve-end.d.ts +6 -0
  140. package/node_modules/yaml/dist/compose/resolve-end.js +39 -0
  141. package/node_modules/yaml/dist/compose/resolve-flow-collection.d.ts +7 -0
  142. package/node_modules/yaml/dist/compose/resolve-flow-collection.js +209 -0
  143. package/node_modules/yaml/dist/compose/resolve-flow-scalar.d.ts +10 -0
  144. package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +228 -0
  145. package/node_modules/yaml/dist/compose/resolve-props.d.ts +23 -0
  146. package/node_modules/yaml/dist/compose/resolve-props.js +148 -0
  147. package/node_modules/yaml/dist/compose/util-contains-newline.d.ts +2 -0
  148. package/node_modules/yaml/dist/compose/util-contains-newline.js +36 -0
  149. package/node_modules/yaml/dist/compose/util-empty-scalar-position.d.ts +2 -0
  150. package/node_modules/yaml/dist/compose/util-empty-scalar-position.js +28 -0
  151. package/node_modules/yaml/dist/compose/util-flow-indent-check.d.ts +3 -0
  152. package/node_modules/yaml/dist/compose/util-flow-indent-check.js +17 -0
  153. package/node_modules/yaml/dist/compose/util-map-includes.d.ts +4 -0
  154. package/node_modules/yaml/dist/compose/util-map-includes.js +15 -0
  155. package/node_modules/yaml/dist/doc/Document.d.ts +141 -0
  156. package/node_modules/yaml/dist/doc/Document.js +337 -0
  157. package/node_modules/yaml/dist/doc/anchors.d.ts +24 -0
  158. package/node_modules/yaml/dist/doc/anchors.js +76 -0
  159. package/node_modules/yaml/dist/doc/applyReviver.d.ts +9 -0
  160. package/node_modules/yaml/dist/doc/applyReviver.js +57 -0
  161. package/node_modules/yaml/dist/doc/createNode.d.ts +17 -0
  162. package/node_modules/yaml/dist/doc/createNode.js +90 -0
  163. package/node_modules/yaml/dist/doc/directives.d.ts +49 -0
  164. package/node_modules/yaml/dist/doc/directives.js +178 -0
  165. package/node_modules/yaml/dist/errors.d.ts +21 -0
  166. package/node_modules/yaml/dist/errors.js +62 -0
  167. package/node_modules/yaml/dist/index.d.ts +25 -0
  168. package/node_modules/yaml/dist/index.js +50 -0
  169. package/node_modules/yaml/dist/log.d.ts +3 -0
  170. package/node_modules/yaml/dist/log.js +19 -0
  171. package/node_modules/yaml/dist/nodes/Alias.d.ts +29 -0
  172. package/node_modules/yaml/dist/nodes/Alias.js +120 -0
  173. package/node_modules/yaml/dist/nodes/Collection.d.ts +73 -0
  174. package/node_modules/yaml/dist/nodes/Collection.js +151 -0
  175. package/node_modules/yaml/dist/nodes/Node.d.ts +53 -0
  176. package/node_modules/yaml/dist/nodes/Node.js +40 -0
  177. package/node_modules/yaml/dist/nodes/Pair.d.ts +22 -0
  178. package/node_modules/yaml/dist/nodes/Pair.js +39 -0
  179. package/node_modules/yaml/dist/nodes/Scalar.d.ts +47 -0
  180. package/node_modules/yaml/dist/nodes/Scalar.js +27 -0
  181. package/node_modules/yaml/dist/nodes/YAMLMap.d.ts +53 -0
  182. package/node_modules/yaml/dist/nodes/YAMLMap.js +147 -0
  183. package/node_modules/yaml/dist/nodes/YAMLSeq.d.ts +60 -0
  184. package/node_modules/yaml/dist/nodes/YAMLSeq.js +115 -0
  185. package/node_modules/yaml/dist/nodes/addPairToJSMap.d.ts +4 -0
  186. package/node_modules/yaml/dist/nodes/addPairToJSMap.js +65 -0
  187. package/node_modules/yaml/dist/nodes/identity.d.ts +23 -0
  188. package/node_modules/yaml/dist/nodes/identity.js +53 -0
  189. package/node_modules/yaml/dist/nodes/toJS.d.ts +29 -0
  190. package/node_modules/yaml/dist/nodes/toJS.js +39 -0
  191. package/node_modules/yaml/dist/options.d.ts +350 -0
  192. package/node_modules/yaml/dist/parse/cst-scalar.d.ts +64 -0
  193. package/node_modules/yaml/dist/parse/cst-scalar.js +218 -0
  194. package/node_modules/yaml/dist/parse/cst-stringify.d.ts +8 -0
  195. package/node_modules/yaml/dist/parse/cst-stringify.js +63 -0
  196. package/node_modules/yaml/dist/parse/cst-visit.d.ts +39 -0
  197. package/node_modules/yaml/dist/parse/cst-visit.js +99 -0
  198. package/node_modules/yaml/dist/parse/cst.d.ts +109 -0
  199. package/node_modules/yaml/dist/parse/cst.js +112 -0
  200. package/node_modules/yaml/dist/parse/lexer.d.ts +87 -0
  201. package/node_modules/yaml/dist/parse/lexer.js +723 -0
  202. package/node_modules/yaml/dist/parse/line-counter.d.ts +22 -0
  203. package/node_modules/yaml/dist/parse/line-counter.js +41 -0
  204. package/node_modules/yaml/dist/parse/parser.d.ts +84 -0
  205. package/node_modules/yaml/dist/parse/parser.js +980 -0
  206. package/node_modules/yaml/dist/public-api.d.ts +44 -0
  207. package/node_modules/yaml/dist/public-api.js +107 -0
  208. package/node_modules/yaml/dist/schema/Schema.d.ts +17 -0
  209. package/node_modules/yaml/dist/schema/Schema.js +39 -0
  210. package/node_modules/yaml/dist/schema/common/map.d.ts +2 -0
  211. package/node_modules/yaml/dist/schema/common/map.js +19 -0
  212. package/node_modules/yaml/dist/schema/common/null.d.ts +4 -0
  213. package/node_modules/yaml/dist/schema/common/null.js +17 -0
  214. package/node_modules/yaml/dist/schema/common/seq.d.ts +2 -0
  215. package/node_modules/yaml/dist/schema/common/seq.js +19 -0
  216. package/node_modules/yaml/dist/schema/common/string.d.ts +2 -0
  217. package/node_modules/yaml/dist/schema/common/string.js +16 -0
  218. package/node_modules/yaml/dist/schema/core/bool.d.ts +4 -0
  219. package/node_modules/yaml/dist/schema/core/bool.js +21 -0
  220. package/node_modules/yaml/dist/schema/core/float.d.ts +4 -0
  221. package/node_modules/yaml/dist/schema/core/float.js +47 -0
  222. package/node_modules/yaml/dist/schema/core/int.d.ts +4 -0
  223. package/node_modules/yaml/dist/schema/core/int.js +42 -0
  224. package/node_modules/yaml/dist/schema/core/schema.d.ts +1 -0
  225. package/node_modules/yaml/dist/schema/core/schema.js +25 -0
  226. package/node_modules/yaml/dist/schema/json/schema.d.ts +2 -0
  227. package/node_modules/yaml/dist/schema/json/schema.js +64 -0
  228. package/node_modules/yaml/dist/schema/json-schema.d.ts +69 -0
  229. package/node_modules/yaml/dist/schema/tags.d.ts +48 -0
  230. package/node_modules/yaml/dist/schema/tags.js +99 -0
  231. package/node_modules/yaml/dist/schema/types.d.ts +92 -0
  232. package/node_modules/yaml/dist/schema/yaml-1.1/binary.d.ts +2 -0
  233. package/node_modules/yaml/dist/schema/yaml-1.1/binary.js +70 -0
  234. package/node_modules/yaml/dist/schema/yaml-1.1/bool.d.ts +7 -0
  235. package/node_modules/yaml/dist/schema/yaml-1.1/bool.js +29 -0
  236. package/node_modules/yaml/dist/schema/yaml-1.1/float.d.ts +4 -0
  237. package/node_modules/yaml/dist/schema/yaml-1.1/float.js +50 -0
  238. package/node_modules/yaml/dist/schema/yaml-1.1/int.d.ts +5 -0
  239. package/node_modules/yaml/dist/schema/yaml-1.1/int.js +76 -0
  240. package/node_modules/yaml/dist/schema/yaml-1.1/merge.d.ts +9 -0
  241. package/node_modules/yaml/dist/schema/yaml-1.1/merge.js +71 -0
  242. package/node_modules/yaml/dist/schema/yaml-1.1/omap.d.ts +22 -0
  243. package/node_modules/yaml/dist/schema/yaml-1.1/omap.js +77 -0
  244. package/node_modules/yaml/dist/schema/yaml-1.1/pairs.d.ts +10 -0
  245. package/node_modules/yaml/dist/schema/yaml-1.1/pairs.js +82 -0
  246. package/node_modules/yaml/dist/schema/yaml-1.1/schema.d.ts +1 -0
  247. package/node_modules/yaml/dist/schema/yaml-1.1/schema.js +41 -0
  248. package/node_modules/yaml/dist/schema/yaml-1.1/set.d.ts +28 -0
  249. package/node_modules/yaml/dist/schema/yaml-1.1/set.js +96 -0
  250. package/node_modules/yaml/dist/schema/yaml-1.1/timestamp.d.ts +6 -0
  251. package/node_modules/yaml/dist/schema/yaml-1.1/timestamp.js +105 -0
  252. package/node_modules/yaml/dist/stringify/foldFlowLines.d.ts +34 -0
  253. package/node_modules/yaml/dist/stringify/foldFlowLines.js +151 -0
  254. package/node_modules/yaml/dist/stringify/stringify.d.ts +21 -0
  255. package/node_modules/yaml/dist/stringify/stringify.js +132 -0
  256. package/node_modules/yaml/dist/stringify/stringifyCollection.d.ts +17 -0
  257. package/node_modules/yaml/dist/stringify/stringifyCollection.js +155 -0
  258. package/node_modules/yaml/dist/stringify/stringifyComment.d.ts +10 -0
  259. package/node_modules/yaml/dist/stringify/stringifyComment.js +24 -0
  260. package/node_modules/yaml/dist/stringify/stringifyDocument.d.ts +4 -0
  261. package/node_modules/yaml/dist/stringify/stringifyDocument.js +87 -0
  262. package/node_modules/yaml/dist/stringify/stringifyNumber.d.ts +2 -0
  263. package/node_modules/yaml/dist/stringify/stringifyNumber.js +27 -0
  264. package/node_modules/yaml/dist/stringify/stringifyPair.d.ts +3 -0
  265. package/node_modules/yaml/dist/stringify/stringifyPair.js +152 -0
  266. package/node_modules/yaml/dist/stringify/stringifyString.d.ts +9 -0
  267. package/node_modules/yaml/dist/stringify/stringifyString.js +338 -0
  268. package/node_modules/yaml/dist/test-events.d.ts +4 -0
  269. package/node_modules/yaml/dist/test-events.js +134 -0
  270. package/node_modules/yaml/dist/util.d.ts +16 -0
  271. package/node_modules/yaml/dist/util.js +28 -0
  272. package/node_modules/yaml/dist/visit.d.ts +102 -0
  273. package/node_modules/yaml/dist/visit.js +236 -0
  274. package/node_modules/yaml/package.json +97 -0
  275. package/node_modules/yaml/util.js +2 -0
  276. package/package.json +54 -0
  277. package/profiles/card.md +65 -0
  278. package/profiles/pillars.md +31 -0
  279. package/profiles/system.md +56 -0
  280. package/schemas/card.schema.json +30 -0
  281. package/skills/gcf-adopt/SKILL.md +41 -0
  282. package/skills/gcf-audit/SKILL.md +27 -0
  283. package/skills/gcf-card/SKILL.md +35 -0
  284. package/skills/gcf-debate/SKILL.md +33 -0
  285. package/skills/gcf-ingest/SKILL.md +38 -0
  286. package/skills/gcf-init/SKILL.md +32 -0
  287. package/skills/gcf-verdict/SKILL.md +32 -0
  288. package/skills/gcf-vision/SKILL.md +33 -0
  289. package/templates/PILLARS.md +8 -0
  290. package/templates/backlog.md +4 -0
  291. package/templates/card.md +34 -0
  292. package/templates/cnlp.config.json +8 -0
  293. package/templates/config.yml +8 -0
  294. package/templates/system.md +22 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oleg (VirtualMaestro)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # gcf-aif-ext
2
+
3
+ The Game Context Framework as an [AI Factory](https://github.com/lee-to/ai-factory) extension: a documentary layer for building games with an AI agent, where the documents hold **state instead of history**.
4
+
5
+ One overwritable document per subsystem. Feature cards that collapse into those documents on a human playtest verdict and then cease to exist. A graveyard that keeps the reasons things were killed. Read rules that keep the token cost of a task flat as the project ages.
6
+
7
+ Runtime-agnostic — everything here works on any AI Factory runtime. The Claude-Code-specific orchestration lives in [`gcf-harness-aif-ext`](../gcf-harness-aif-ext), which is optional.
8
+
9
+ ## Requirements
10
+
11
+ - AI Factory 2.x
12
+ - Node.js ≥ 22.14
13
+ - git (the enforcement gate is a pre-commit hook)
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ ai-factory extension add <path-to>/packages/gcf-aif-ext
19
+ ai-factory gcf init # seeds gcf/ and cnlp.config.json
20
+ ai-factory gcf hooks-install # installs the pre-commit gate
21
+ ```
22
+
23
+ Then `/gcf-vision` for a brand-new game, `/gcf-adopt` for a project that already has code, `/gcf-ingest <gdd.md>` for an existing design doc, or `/gcf-card` to start a feature by hand.
24
+
25
+ ## What it adds
26
+
27
+ **Skills** — `gcf-init`, `gcf-adopt`, `gcf-card`, `gcf-verdict`, `gcf-audit`, `gcf-vision`, `gcf-debate`, `gcf-ingest`.
28
+
29
+ **A deterministic core** behind `ai-factory gcf …` — `init`, `index`, `context`, `audit`, `playtest`, `verdict`, `anchor`, `lint`, `hooks-install`. These are plain Node scripts with no LLM in the loop: the same functions the skills call, the pre-commit gate runs, and the harness gates against.
30
+
31
+ **A pre-commit gate** that breaks the commit on a terminal status stored in a card file, a malformed `[until:]` annotation, a card built on an unplayed dependency, a dependency that names no known card, or a П2 linter finding in a subsystem document. It regenerates `gcf/INDEX.md` and stages it for you.
32
+
33
+ **CNLP profiles and a card schema** for the documents the agent reads, plus injections into `aif-plan`, `aif-review` and `aif-commit`.
34
+
35
+ ## Docs
36
+
37
+ Full documentation lives in the [repository](https://github.com/VirtualMaestro/GCF): the [Usage Guide](https://github.com/VirtualMaestro/GCF/blob/main/docs/Usage-Guide.md) (RU) for day-to-day work, and [GCF-PRD](https://github.com/VirtualMaestro/GCF/blob/main/docs/GCF-PRD.md) for the spec — principles, data model, lifecycle, enforcement.
38
+
39
+ ## License
40
+
41
+ MIT
@@ -0,0 +1,173 @@
1
+ // CLI wrapper over the deterministic core (GCF-PRD §10.1): harness and human call
2
+ // the same functions. Registered by AIF via extension.json `commands`.
3
+ import { mkdir, copyFile, readFile } from 'node:fs/promises';
4
+ import { existsSync } from 'node:fs';
5
+ import { spawnSync } from 'node:child_process';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const EXT_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
10
+ const lib = (name) => import(new URL(`../lib/${name}.js`, import.meta.url));
11
+
12
+ // Must stay byte-identical to the comment line inside the shim below: hooks-install
13
+ // refuses to overwrite a pre-commit hook that does not carry it.
14
+ const MARKER = 'gcf-aif-ext pre-commit gate';
15
+
16
+ const out = (json, value, human) => console.log(json ? JSON.stringify(value, null, 2) : human(value));
17
+
18
+ export function register(program) {
19
+ const gcf = program.command('gcf').description('Game Context Framework core');
20
+
21
+ gcf.command('init')
22
+ .description('Create the gcf/ skeleton from templates (idempotent, never overwrites)')
23
+ .action(async () => {
24
+ const root = path.join(process.cwd(), 'gcf');
25
+ for (const dir of ['cards', 'systems', 'source', '.metrics']) {
26
+ await mkdir(path.join(root, dir), { recursive: true });
27
+ }
28
+ const seed = [
29
+ ['PILLARS.md', 'PILLARS.md'], ['backlog.md', 'backlog.md'], ['config.yml', 'config.yml'],
30
+ ];
31
+ for (const [tpl, dst] of seed) {
32
+ const target = path.join(root, dst);
33
+ if (!existsSync(target)) await copyFile(path.join(EXT_ROOT, 'templates', tpl), target);
34
+ }
35
+ for (const [name, header] of [
36
+ ['graveyard.md', '# Graveyard — one line per killed idea, written by gcf verdict kill\n'],
37
+ ['open-questions.md', '# Open questions — records with id/lens/sections/sys/class/proposed; answers move out, records die\n'],
38
+ ]) {
39
+ const target = path.join(root, name);
40
+ if (!existsSync(target)) await (await import('node:fs/promises')).writeFile(target, header, 'utf8');
41
+ }
42
+ const cnlpCfg = path.join(process.cwd(), 'cnlp.config.json');
43
+ if (!existsSync(cnlpCfg)) await copyFile(path.join(EXT_ROOT, 'templates', 'cnlp.config.json'), cnlpCfg);
44
+ const { writeIndex } = await lib('index-gen');
45
+ await writeIndex(root);
46
+ console.log('gcf/ ready. Edit PILLARS.md, then add candidates to backlog.md.');
47
+ });
48
+
49
+ gcf.command('index')
50
+ .description('Regenerate gcf/INDEX.md (deterministic)')
51
+ .action(async () => {
52
+ const { gcfRoot } = await lib('gcf-paths');
53
+ const { writeIndex } = await lib('index-gen');
54
+ await writeIndex(gcfRoot());
55
+ console.log('INDEX.md regenerated');
56
+ });
57
+
58
+ gcf.command('context <card-id>')
59
+ .description('Assemble the readset for a card (GCF-PRD §9.1)')
60
+ .option('--json', 'machine output')
61
+ .action(async (cardId, opts) => {
62
+ const { buildContext } = await lib('context');
63
+ const ctx = await buildContext(process.cwd(), cardId);
64
+ // Metric 1 (GCF-PRD §14): readset size over time, one csv line per call.
65
+ const { appendFile, mkdir } = await import('node:fs/promises');
66
+ const metricsDir = path.join(process.cwd(), 'gcf', '.metrics');
67
+ await mkdir(metricsDir, { recursive: true });
68
+ await appendFile(path.join(metricsDir, 'context-log.csv'),
69
+ `${new Date().toISOString().slice(0, 10)},${cardId},${ctx.files.length},${ctx.total_tokens_est}\n`, 'utf8');
70
+ out(opts.json, ctx, (c) =>
71
+ [
72
+ ...c.files.map((f) => `${f.path} (${f.tokens_est})`),
73
+ ...(c.graveyard_hits.length ? ['graveyard hits:', ...c.graveyard_hits] : []),
74
+ `total ~${c.total_tokens_est} / ${c.budget}${c.over_budget ? ' OVER BUDGET' : ''}`,
75
+ ].join('\n'));
76
+ if (ctx.over_budget) process.exitCode = 2;
77
+ });
78
+
79
+ gcf.command('audit')
80
+ .description('Deterministic consistency audit (GCF-PRD §10.1)')
81
+ .option('--json', 'machine output')
82
+ .action(async (opts) => {
83
+ const { audit } = await lib('audit-core');
84
+ const report = await audit(process.cwd());
85
+ // Metric 2 (GCF-PRD §14): findings per audit run.
86
+ const findings = report.stale_built.length + report.tune_limit.length
87
+ + report.until.filter((u) => u.status === 'stale').length + Object.keys(report.linter).length;
88
+ const { appendFile, mkdir } = await import('node:fs/promises');
89
+ const metricsDir = path.join(process.cwd(), 'gcf', '.metrics');
90
+ if (existsSync(path.join(process.cwd(), 'gcf'))) {
91
+ await mkdir(metricsDir, { recursive: true });
92
+ await appendFile(path.join(metricsDir, 'audit-log.csv'), `${new Date().toISOString().slice(0, 10)},${findings}\n`, 'utf8');
93
+ }
94
+ out(opts.json, report, (r) => r.first_line);
95
+ if (report.first_line !== 'clean') process.exitCode = 1;
96
+ });
97
+
98
+ gcf.command('playtest')
99
+ .description('List built cards with their miss-criterion questions')
100
+ .option('--json', 'machine output')
101
+ .action(async (opts) => {
102
+ const { gcfRoot } = await lib('gcf-paths');
103
+ const { playtestList } = await lib('playtest-list');
104
+ const list = await playtestList(gcfRoot());
105
+ out(opts.json, list, (l) => (l.length
106
+ ? l.map((c) => `${c.id} ${c.slug} [${c.system.join(',')}]: ${c.question}`).join('\n')
107
+ : 'nothing in built — nothing to playtest'));
108
+ });
109
+
110
+ gcf.command('verdict <card-id> <verdict>')
111
+ .description('Apply a human verdict: keep | tune | kill (GCF-PRD §8)')
112
+ .option('--note <text>', 'tune remark or kill reason')
113
+ .option('--no-commit', 'skip the verdict commit (tests only)')
114
+ .action(async (cardId, verdict, opts) => {
115
+ const { applyVerdict } = await lib('verdict-fs');
116
+ const res = await applyVerdict(process.cwd(), cardId, verdict, { note: opts.note ?? '', commit: opts.commit });
117
+ console.log(`${res.verdict} ${res.card} → ${res.systems.join(',')}${res.hash ? ` (${res.hash.slice(0, 7)})` : ''}`);
118
+ });
119
+
120
+ gcf.command('anchor <file>')
121
+ .description('Import a GDD into gcf/source/ with stable section anchors; on re-import prints the delta')
122
+ .option('--json', 'machine output')
123
+ .action(async (file, opts) => {
124
+ const { splitSections, importSource, loadAnchorMap, classifyDelta } = await lib('anchors');
125
+ const base = path.basename(file);
126
+ const oldMap = await loadAnchorMap(process.cwd(), base);
127
+ const { readFile } = await import('node:fs/promises');
128
+ const sections = splitSections(await readFile(file, 'utf8'));
129
+ const delta = oldMap?.anchors ? classifyDelta(oldMap.anchors, sections) : null;
130
+ await importSource(process.cwd(), file);
131
+ const rebaselined = Boolean(oldMap?.rebaselined);
132
+ const result = { file: `gcf/source/${base}`, sections: sections.map((s) => ({ anchor: s.anchor, heading: s.heading })), delta, rebaselined };
133
+ out(opts.json, result, (r) => [
134
+ ...r.sections.map((s) => `${s.anchor} ${s.heading ?? '(preamble)'}`),
135
+ r.delta
136
+ ? `delta: +${r.delta.added.length} ~${r.delta.changed.length} -${r.delta.removed.length} renamed ${r.delta.renamed.length}`
137
+ : (r.rebaselined ? 're-baselined: the stored anchor map predates the current hashing, no delta computed' : 'first import'),
138
+ ].join('\n'));
139
+ });
140
+
141
+ gcf.command('hooks-install')
142
+ .description('Install the gcf pre-commit gate into the repository hooks dir (idempotent)')
143
+ .action(async () => {
144
+ const { writeFile, chmod } = await import('node:fs/promises');
145
+ // --git-path resolves the hooks dir for a plain repo, a worktree and a submodule alike,
146
+ // where a bare existsSync('.git') passes on a worktree and then fails with ENOTDIR.
147
+ const res = spawnSync('git', ['rev-parse', '--git-path', 'hooks'], { cwd: process.cwd(), encoding: 'utf8' });
148
+ if (res.status !== 0) throw new Error('not a git repository');
149
+ const hookScript = path.join(EXT_ROOT, 'hooks', 'pre-commit.js');
150
+ const shim = `#!/bin/sh\n# gcf-aif-ext pre-commit gate\nnode "${hookScript.replaceAll('\\', '/')}" || exit 1\n`;
151
+ const target = path.resolve(process.cwd(), res.stdout.trim(), 'pre-commit');
152
+ if (existsSync(target) && !(await readFile(target, 'utf8')).includes(MARKER)) {
153
+ // Brownfield repos usually arrive with husky or lefthook already owning pre-commit,
154
+ // so the refusal hands over the exact line to chain instead of just naming the file.
155
+ throw new Error(`${target} already exists and is not ours - back it up, or chain the gate from it with:\n`
156
+ + `node "${hookScript.replaceAll('\\', '/')}" || exit 1`);
157
+ }
158
+ await mkdir(path.dirname(target), { recursive: true });
159
+ await writeFile(target, shim, 'utf8');
160
+ await chmod(target, 0o755).catch(() => {});
161
+ console.log(`pre-commit hook installed → ${target}`);
162
+ });
163
+
164
+ gcf.command('lint <file>')
165
+ .description('П2 linter over one document (GCF-PRD §13.2)')
166
+ .action(async (file) => {
167
+ const { lint } = await lib('linter');
168
+ const { issues } = lint(await readFile(file, 'utf8'));
169
+ for (const i of issues) console.log(`${file}:${i.line}: ${i.rule}: ${i.text}`);
170
+ if (issues.length) process.exitCode = 1;
171
+ else console.log('clean');
172
+ });
173
+ }
package/extension.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/lee-to/ai-factory/2.x/schemas/extension.schema.json",
3
+ "name": "gcf-aif-ext",
4
+ "version": "0.2.0",
5
+ "description": "Game Context Framework for AI Factory",
6
+ "skills": [
7
+ "skills/gcf-init",
8
+ "skills/gcf-adopt",
9
+ "skills/gcf-card",
10
+ "skills/gcf-verdict",
11
+ "skills/gcf-audit",
12
+ "skills/gcf-vision",
13
+ "skills/gcf-debate",
14
+ "skills/gcf-ingest"
15
+ ],
16
+ "commands": [
17
+ { "name": "gcf", "description": "Game Context Framework core", "module": "commands/gcf.js" }
18
+ ],
19
+ "injections": [
20
+ { "target": "aif-plan", "position": "append", "file": "./injections/plan-context.md" },
21
+ { "target": "aif-review", "position": "append", "file": "./injections/review-pillars.md" },
22
+ { "target": "aif-commit", "position": "append", "file": "./injections/commit-status.md" }
23
+ ]
24
+ }
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ // GCF pre-commit gate (GCF-PRD §13.1), cheap checks first, one-line reason on failure.
3
+ // Runs from the repository root (git invokes hooks there). Silent no-op without gcf/.
4
+ // The gate reads the WORKING TREE, so it only runs when the commit touches gcf/ and
5
+ // refuses to judge a gcf/ whose working tree and index disagree.
6
+ import { existsSync } from 'node:fs';
7
+ import { readFile } from 'node:fs/promises';
8
+ import { spawnSync } from 'node:child_process';
9
+ import path from 'node:path';
10
+ import { listCards, listSystems } from '../lib/gcf-paths.js';
11
+ import { validateCard, validateUntil } from '../lib/validate-card.js';
12
+ import { stemMatches } from '../lib/id.js';
13
+ import { writeIndex } from '../lib/index-gen.js';
14
+ import { lint } from '../lib/linter.js';
15
+
16
+ const projectDir = process.cwd();
17
+ const root = path.join(projectDir, 'gcf');
18
+ if (!existsSync(root)) process.exit(0);
19
+
20
+ const fail = (msg) => {
21
+ console.error(`gcf pre-commit: ${msg}`);
22
+ process.exit(1);
23
+ };
24
+
25
+ // null when git itself failed (unborn HEAD, no work tree) — then fall through to the gate
26
+ // rather than letting a broken probe wave a commit past every check.
27
+ function git(args) {
28
+ const res = spawnSync('git', args, { cwd: projectDir, encoding: 'utf8' });
29
+ return res.status === 0 ? res.stdout.trim() : null;
30
+ }
31
+
32
+ const staged = git(['diff', '--cached', '--name-only', '--', root]);
33
+ if (staged === '') process.exit(0); // this commit does not touch gcf/
34
+ if (staged !== null) {
35
+ // Untracked counts: listCards reads the directory, so an uncommitted card file would be
36
+ // validated and written into the staged INDEX while not being in the repository at all.
37
+ const unstaged = git(['diff', '--name-only', '--', root]);
38
+ const untracked = git(['ls-files', '--others', '--exclude-standard', '--', root]);
39
+ if (unstaged || untracked) {
40
+ fail('gcf/ has changes outside the index — stage them, or commit gcf/ separately (the gate reads the working tree)');
41
+ }
42
+ }
43
+
44
+ const cards = await listCards(root);
45
+ const cardIds = new Set(cards.map((c) => c.data.id));
46
+ const systemDocs = await listSystems(root);
47
+
48
+ // 1-2: frontmatter validity, storable statuses, until grammar, filename invariant.
49
+ for (const c of cards) {
50
+ const errors = [
51
+ ...validateCard(c.data),
52
+ ...validateUntil(c.body),
53
+ ...(stemMatches(c.file, c.data.id, c.data.slug) ? [] : [`filename must be ${c.data.id}-${c.data.slug}.md`]),
54
+ ];
55
+ if (errors.length) fail(`${c.name}: ${errors[0]}`);
56
+ }
57
+ for (const s of systemDocs) {
58
+ const errors = validateUntil(s.body);
59
+ if (errors.length) fail(`systems/${s.name}: ${errors[0]}`);
60
+ }
61
+
62
+ // 4: depends reference known cards; a built card must not sit on un-collapsed depends.
63
+ // A collapsed card leaves no file, so its trace is the verdict commit or the graveyard line.
64
+ const verdictLog = git(['log', '--grep', '^gcf(verdict):', '--format=%s']) ?? '';
65
+ const gyFile = path.join(root, 'graveyard.md');
66
+ const graveyard = existsSync(gyFile) ? await readFile(gyFile, 'utf8') : '';
67
+ const collapsed = (id) => new RegExp(`^gcf\\(verdict\\): \\w+ ${id}\\b`, 'm').test(verdictLog)
68
+ || new RegExp(`^- ${id} \\|`, 'm').test(graveyard);
69
+
70
+ for (const c of cards) {
71
+ for (const dep of c.data.depends ?? []) {
72
+ if (cardIds.has(dep)) {
73
+ if (c.data.status === 'built') {
74
+ fail(`${c.data.id} is built on top of unplayed ${dep} (collapse it first — GCF-PRD §7.2)`);
75
+ }
76
+ } else if (!collapsed(dep)) {
77
+ fail(`${c.data.id} depends on unknown ${dep} (no card, no verdict, no graveyard line)`);
78
+ }
79
+ }
80
+ }
81
+
82
+ // 5: П2 linter over systems docs.
83
+ for (const s of systemDocs) {
84
+ const { issues } = lint(s.body);
85
+ if (issues.length) fail(`systems/${s.name}:${issues[0].line}: ${issues[0].rule}: ${issues[0].text}`);
86
+ }
87
+
88
+ // 3: regenerate INDEX and stage it (deterministic artifact, not a human error).
89
+ await writeIndex(root);
90
+ spawnSync('git', ['add', '--', path.join(root, 'INDEX.md')], { cwd: projectDir, stdio: 'ignore' });
91
+ process.exit(0);
@@ -0,0 +1,6 @@
1
+ ## GCF: card status before commit (from gcf-aif-ext)
2
+
3
+ gcf_commit:
4
+ - when `gcf/` exists and the commit implements a card, verify the card frontmatter is current
5
+ - an implementation commit that completes a build flips `status:` to `built` and records `built_at:` with the commit hash in the same commit
6
+ - do not commit a card file whose `status:` is `validated` or `killed`: those are terminal and never stored
@@ -0,0 +1,8 @@
1
+ ## GCF: readset before planning (from gcf-aif-ext)
2
+
3
+ gcf_gate:
4
+ - when `gcf/` exists and the task names a card, run `ai-factory gcf context <card-id>` first
5
+ - the returned file list is the readset: read those files and nothing else under `gcf/`
6
+ - `over_budget: true` in the result: stop and record a D1 question instead of planning
7
+ - a card in `depends:` that still exists as a file is unplayed: do not plan on top of it, stop
8
+ - shape the plan by the card slots: behavior, contract, feel envelope, interactions, negative constraints, miss criterion
@@ -0,0 +1,7 @@
1
+ ## GCF: pillar and constraint review (from gcf-aif-ext)
2
+
3
+ gcf_review:
4
+ - when `gcf/` exists and the change implements a card, read `gcf/PILLARS.md` and the card
5
+ - check the diff against every negative constraint of the card: name each violation with file and line
6
+ - check the diff against each pillar's review question: a contradiction is a finding, not a note
7
+ - numeric feel values in code outside the card's envelope: report the value and the envelope bounds
package/lib/anchors.js ADDED
@@ -0,0 +1,109 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import path from 'node:path';
5
+
6
+ // GDD anchoring (GCF-PRD §11.2.1): a section id is the hash of its normalized heading —
7
+ // stable under reorder, not under rename. Re-import matches orphaned anchors by body
8
+ // hash and reports a rename instead of delete+add.
9
+
10
+ const norm = (s) => String(s).toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').trim();
11
+ const h8 = (s) => createHash('sha1').update(s, 'utf8').digest('hex').slice(0, 8);
12
+ // A body keeps its punctuation and structure, unlike a heading: `norm` would hash
13
+ // `- a` + `- b` the same as `a, b`, hiding a real edit from classifyDelta.
14
+ const bodyNorm = (s) => String(s).replace(/\r\n/g, '\n').replace(/[ \t]+$/gm, '').trim();
15
+ const EMPTY_HASH = h8('');
16
+ // Bumped whenever body hashing changes: a stored map from an older scheme cannot be
17
+ // compared against fresh hashes, and reporting every section as changed would flood the
18
+ // human with D1 questions about a GDD nobody edited.
19
+ export const HASH_VERSION = 2;
20
+
21
+ /** Split markdown into heading-addressed sections. Text before the first heading is `GDD#preamble`. */
22
+ export function splitSections(markdown) {
23
+ const lines = String(markdown).split('\n');
24
+ const sections = [];
25
+ let current = { heading: null, level: 0, lines: [] };
26
+ const flush = () => {
27
+ if (current.heading === null && !current.lines.join('').trim()) return;
28
+ sections.push(current);
29
+ };
30
+ for (const line of lines) {
31
+ const m = line.match(/^(#{1,6})\s+(.+?)\s*$/);
32
+ if (m) {
33
+ flush();
34
+ current = { heading: m[2], level: m[1].length, lines: [] };
35
+ } else current.lines.push(line);
36
+ }
37
+ flush();
38
+
39
+ const seen = new Map();
40
+ return sections.map((s) => {
41
+ const base = s.heading === null ? 'preamble' : h8(norm(s.heading));
42
+ const n = seen.get(base) ?? 0;
43
+ seen.set(base, n + 1);
44
+ const body = s.lines.join('\n');
45
+ return {
46
+ anchor: `GDD#${n ? `${base}-${n}` : base}`,
47
+ heading: s.heading,
48
+ level: s.level,
49
+ body,
50
+ body_hash: h8(bodyNorm(body)),
51
+ };
52
+ });
53
+ }
54
+
55
+ /** Copy a GDD into gcf/source/ and write its anchor map next to it. */
56
+ export async function importSource(projectDir, file) {
57
+ const root = path.join(path.resolve(projectDir), 'gcf');
58
+ const content = await readFile(file, 'utf8');
59
+ const sections = splitSections(content);
60
+ const base = path.basename(file);
61
+ await mkdir(path.join(root, 'source'), { recursive: true });
62
+ await writeFile(path.join(root, 'source', base), content, 'utf8');
63
+ const anchors = Object.fromEntries(sections.map((s) => [s.anchor, { heading: s.heading, body_hash: s.body_hash }]));
64
+ const map = { hash_version: HASH_VERSION, anchors };
65
+ await writeFile(path.join(root, 'source', `${base}.anchors.json`), JSON.stringify(map, null, 2), 'utf8');
66
+ return { file: `gcf/source/${base}`, sections };
67
+ }
68
+
69
+ /**
70
+ * Delta classification for a re-import (GCF-PRD §11.6): added | changed | removed | renamed.
71
+ * A removed anchor whose body hash reappears under a new anchor is a rename.
72
+ */
73
+ export function classifyDelta(oldMap, sections) {
74
+ const delta = { added: [], changed: [], removed: [], renamed: [] };
75
+ const newByAnchor = new Map(sections.map((s) => [s.anchor, s]));
76
+
77
+ for (const s of sections) {
78
+ const old = oldMap[s.anchor];
79
+ if (!old) continue;
80
+ if (old.body_hash !== s.body_hash) delta.changed.push(s.anchor);
81
+ }
82
+ const missing = Object.entries(oldMap).filter(([a]) => !newByAnchor.has(a));
83
+ const fresh = sections.filter((s) => !(s.anchor in oldMap));
84
+ // Each fresh section can be claimed once: without consuming the match, one section
85
+ // becomes the rename target of every missing anchor that shares its hash.
86
+ const pool = [...fresh];
87
+ for (const [anchor, old] of missing) {
88
+ // An empty body hashes the same everywhere, so it is evidence of nothing.
89
+ const i = old.body_hash === EMPTY_HASH ? -1 : pool.findIndex((s) => s.body_hash === old.body_hash);
90
+ if (i >= 0) delta.renamed.push({ from: anchor, to: pool.splice(i, 1)[0].anchor });
91
+ else delta.removed.push(anchor);
92
+ }
93
+ const renamedTo = new Set(delta.renamed.map((r) => r.to));
94
+ delta.added = fresh.map((s) => s.anchor).filter((a) => !renamedTo.has(a));
95
+ return delta;
96
+ }
97
+
98
+ /**
99
+ * Load a previously written anchor map: `{ anchors, rebaselined }`, or null on first import.
100
+ * `rebaselined` means the stored hashes came from an older scheme, so there is no delta to
101
+ * compute — the import re-baselines silently instead of calling every section changed.
102
+ */
103
+ export async function loadAnchorMap(projectDir, baseName) {
104
+ const file = path.join(path.resolve(projectDir), 'gcf', 'source', `${baseName}.anchors.json`);
105
+ if (!existsSync(file)) return null;
106
+ const raw = JSON.parse(await readFile(file, 'utf8'));
107
+ if (raw.hash_version !== HASH_VERSION) return { anchors: null, rebaselined: true };
108
+ return { anchors: raw.anchors ?? {}, rebaselined: false };
109
+ }
@@ -0,0 +1,102 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { listCards, listSystems, readConfig, tokensEst } from './gcf-paths.js';
6
+ import { lint } from './linter.js';
7
+ import { drifted } from './index-gen.js';
8
+
9
+ function git(args, cwd) {
10
+ const res = spawnSync('git', args, { cwd, encoding: 'utf8' });
11
+ return res.status === 0 ? res.stdout.trim() : null;
12
+ }
13
+
14
+ const UNTIL_RE = /\[until:\s*(verdict\(card-\d{3}\)|retune\([a-z0-9-]+\)|manual\([^)]*\))\]/g;
15
+
16
+ /**
17
+ * Deterministic audit core (GCF-PRD §10.1): until triggers, built staleness, tune
18
+ * limits, WIP, PILLARS budget, П2 linter, INDEX drift. No LLM anywhere; the
19
+ * contradiction hunt is a separate optional skill pass.
20
+ */
21
+ export async function audit(projectDir) {
22
+ const root = path.join(path.resolve(projectDir), 'gcf');
23
+ const config = await readConfig(root);
24
+ const cards = await listCards(root);
25
+ const report = { first_line: '', stale_built: [], tune_limit: [], wip: null, pillars: null, until: [], linter: {}, pragmas: [], index_drift: false, illegal_status: [] };
26
+
27
+ // Staleness is measured in commits, so outside a repository there is no answer to give.
28
+ // Without this probe every built card would read as stale and stop the harness.
29
+ const inRepo = git(['rev-parse', '--git-dir'], projectDir) !== null;
30
+
31
+ // Verdict history from commit messages of the fixed format (GCF-PRD §8).
32
+ const verdictLog = git(['log', '--grep', '^gcf(verdict):', '--format=%s'], projectDir) ?? '';
33
+ const verdictFired = (cardId) => new RegExp(`^gcf\\(verdict\\): \\w+ ${cardId}\\b`, 'm').test(verdictLog);
34
+ // ponytail: retune(sys) fires on any tune verdict naming the system; the PRD's
35
+ // feel-section-diff refinement can replace this when it proves too coarse.
36
+ const retuneFired = (system) => new RegExp(`^gcf\\(verdict\\): tune .*→ .*${system}`, 'm').test(verdictLog);
37
+
38
+ for (const c of cards) {
39
+ if (!['proposed', 'built', 'tuning'].includes(c.data.status)) {
40
+ report.illegal_status.push({ id: c.data.id, status: c.data.status });
41
+ }
42
+ if (inRepo && c.data.status === 'built' && c.data.built_at) {
43
+ // git() is null when the ref no longer resolves (rebase, squash, amend, shallow clone).
44
+ // Number(null) is 0, which reads as "fresh" — the one answer that must never be guessed.
45
+ const raw = git(['rev-list', '--count', `${c.data.built_at}..HEAD`], projectDir);
46
+ const n = raw === null ? NaN : Number(raw);
47
+ if (!Number.isFinite(n) || n > config.built_stale_after_commits) {
48
+ report.stale_built.push({ id: c.data.id, commits: Number.isFinite(n) ? n : null, limit: config.built_stale_after_commits });
49
+ }
50
+ }
51
+ if ((c.data.tune_count ?? 0) >= config.tune_limit) {
52
+ report.tune_limit.push({ id: c.data.id, tune_count: c.data.tune_count, limit: config.tune_limit });
53
+ }
54
+ }
55
+
56
+ const wipCount = cards.filter((c) => c.data.status === 'built').length;
57
+ report.wip = { count: wipCount, limit: config.wip_built_limit, over: wipCount > config.wip_built_limit };
58
+
59
+ const pillarsFile = path.join(root, 'PILLARS.md');
60
+ if (existsSync(pillarsFile)) {
61
+ const tokens = tokensEst(await readFile(pillarsFile, 'utf8'));
62
+ report.pillars = { tokens, budget: config.pillars_budget, over: tokens > config.pillars_budget };
63
+ }
64
+
65
+ // Until annotations across cards and systems docs.
66
+ const docs = [
67
+ ...cards.map((c) => ({ rel: `cards/${c.name}`, body: c.body })),
68
+ ...(await listSystems(root)).map((s) => ({ rel: `systems/${s.name}`, body: s.body })),
69
+ ];
70
+ for (const doc of docs) {
71
+ for (const m of doc.body.matchAll(UNTIL_RE)) {
72
+ const trigger = m[1];
73
+ let status = 'open';
74
+ if (trigger.startsWith('verdict(')) status = verdictFired(trigger.slice(8, -1)) ? 'stale' : 'open';
75
+ else if (trigger.startsWith('retune(')) status = retuneFired(trigger.slice(7, -1)) ? 'stale' : 'open';
76
+ else status = 'manual';
77
+ report.until.push({ file: `gcf/${doc.rel}`, trigger, status });
78
+ }
79
+ }
80
+
81
+ // П2 linter over systems docs.
82
+ for (const s of await listSystems(root)) {
83
+ const { issues, pragmas } = lint(s.body);
84
+ if (issues.length) report.linter[`gcf/systems/${s.name}`] = issues;
85
+ report.pragmas.push(...pragmas.map((p) => ({ file: `gcf/systems/${s.name}`, ...p })));
86
+ }
87
+
88
+ report.index_drift = await drifted(root);
89
+
90
+ const alerts = [
91
+ report.illegal_status.length && `${report.illegal_status.length} card(s) in illegal status`,
92
+ report.stale_built.length && `${report.stale_built.length} built card(s) stale — playtest first`,
93
+ report.tune_limit.length && `${report.tune_limit.length} card(s) at tune limit — kill or redesign`,
94
+ report.wip.over && `WIP over limit (${report.wip.count}/${report.wip.limit})`,
95
+ report.until.filter((u) => u.status === 'stale').length && 'stale until-records present',
96
+ report.pillars?.over && 'PILLARS over token budget',
97
+ Object.keys(report.linter).length && 'П2 linter findings in systems docs',
98
+ report.index_drift && 'INDEX drift (regenerate)',
99
+ ].filter(Boolean);
100
+ report.first_line = alerts.length ? alerts.join('; ') : 'clean';
101
+ return report;
102
+ }