universal-dev-standards 6.7.5 → 6.9.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 (213) hide show
  1. package/bin/uds.js +19 -2
  2. package/bundled/ai/standards/acceptance-criteria-traceability.ai.yaml +14 -2
  3. package/bundled/ai/standards/adr-standards.ai.yaml +14 -2
  4. package/bundled/ai/standards/ai-instruction-standards.ai.yaml +6 -6
  5. package/bundled/ai/standards/code-review.ai.yaml +13 -3
  6. package/bundled/ai/standards/commit-message.ai.yaml +8 -4
  7. package/bundled/ai/standards/deferred-item-exit.ai.yaml +225 -0
  8. package/bundled/ai/standards/feature-discovery-standards.ai.yaml +14 -2
  9. package/bundled/ai/standards/governance-layer.ai.yaml +128 -2
  10. package/bundled/ai/standards/logging.ai.yaml +2 -2
  11. package/bundled/ai/standards/retrospective-standards.ai.yaml +14 -2
  12. package/bundled/ai/standards/reverse-engineering-standards.ai.yaml +73 -2
  13. package/bundled/ai/standards/security-standards.ai.yaml +2 -2
  14. package/bundled/ai/standards/spec-driven-development.ai.yaml +14 -2
  15. package/bundled/ai/standards/tech-debt-standards.ai.yaml +87 -3
  16. package/bundled/ai/standards/turn-completion-integrity.ai.yaml +131 -0
  17. package/bundled/core/acceptance-criteria-traceability.md +5 -2
  18. package/bundled/core/adr-standards.md +26 -2
  19. package/bundled/core/ai-instruction-standards.md +9 -7
  20. package/bundled/core/code-review-checklist.md +5 -2
  21. package/bundled/core/context-aware-loading.md +1 -1
  22. package/bundled/core/deferred-item-exit.md +254 -0
  23. package/bundled/core/feature-discovery-standards.md +5 -1
  24. package/bundled/core/governance-layer.md +114 -2
  25. package/bundled/core/retrospective-standards.md +4 -2
  26. package/bundled/core/reverse-engineering-standards.md +81 -2
  27. package/bundled/core/spec-driven-development.md +8 -2
  28. package/bundled/core/tech-debt-standards.md +67 -8
  29. package/bundled/core/turn-completion-integrity.md +196 -0
  30. package/bundled/hooks/check-dangerous-cmd.mjs +60 -0
  31. package/bundled/hooks/check-logging-standard.mjs +59 -0
  32. package/bundled/hooks/check-turn-completion.mjs +233 -0
  33. package/bundled/hooks/inject-standards.mjs +183 -0
  34. package/bundled/hooks/telemetry-wrapper.mjs +77 -0
  35. package/bundled/hooks/turn-completion/detect.mjs +99 -0
  36. package/bundled/hooks/turn-completion/locales/en.mjs +159 -0
  37. package/bundled/hooks/turn-completion/locales/zh-TW.mjs +166 -0
  38. package/bundled/hooks/validate-commit-msg.mjs +104 -0
  39. package/bundled/locales/zh-CN/CHANGELOG.md +65 -3
  40. package/bundled/locales/zh-CN/CLAUDE.md +1 -1
  41. package/bundled/locales/zh-CN/README.md +7 -7
  42. package/bundled/locales/zh-CN/SECURITY.md +1 -1
  43. package/bundled/locales/zh-CN/core/adr-standards.md +1 -1
  44. package/bundled/locales/zh-CN/core/ai-instruction-standards.md +10 -8
  45. package/bundled/locales/zh-CN/core/governance-layer.md +118 -6
  46. package/bundled/locales/zh-CN/core/retrospective-standards.md +1 -1
  47. package/bundled/locales/zh-CN/core/tech-debt-standards.md +71 -4
  48. package/bundled/locales/zh-CN/core/turn-completion-integrity.md +190 -0
  49. package/bundled/locales/zh-CN/docs/CHEATSHEET.md +27 -6
  50. package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +29 -68
  51. package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +172 -24
  52. package/bundled/locales/zh-CN/docs/USAGE-MODES-COMPARISON.md +1 -2
  53. package/bundled/locales/zh-CN/integrations/google-antigravity/{INSTRUCTIONS.md → AGENTS.md} +1 -1
  54. package/bundled/locales/zh-CN/integrations/google-antigravity/README.md +3 -3
  55. package/bundled/locales/zh-CN/skills/agents/README.md +1 -1
  56. package/bundled/locales/zh-CN/skills/atdd-assistant/SKILL.md +2 -0
  57. package/bundled/locales/zh-CN/skills/bdd-assistant/SKILL.md +2 -0
  58. package/bundled/locales/zh-CN/skills/brainstorm-assistant/SKILL.md +22 -12
  59. package/bundled/locales/zh-CN/skills/brainstorm-assistant/guide.md +12 -9
  60. package/bundled/locales/zh-CN/skills/code-review-assistant/SKILL.md +1 -0
  61. package/bundled/locales/zh-CN/skills/commands/brainstorm.md +17 -13
  62. package/bundled/locales/zh-CN/skills/commands/config.md +0 -1
  63. package/bundled/locales/zh-CN/skills/commands/init.md +1 -2
  64. package/bundled/locales/zh-CN/skills/commit-standards/SKILL.md +2 -0
  65. package/bundled/locales/zh-CN/skills/contract-test-assistant/SKILL.md +1 -0
  66. package/bundled/locales/zh-CN/skills/dev-methodology/SKILL.md +2 -0
  67. package/bundled/locales/zh-CN/skills/observability-assistant/SKILL.md +1 -0
  68. package/bundled/locales/zh-CN/skills/project-structure-guide/SKILL.md +1 -0
  69. package/bundled/locales/zh-CN/skills/release-standards/SKILL.md +3 -0
  70. package/bundled/locales/zh-CN/skills/requirement-assistant/SKILL.md +2 -0
  71. package/bundled/locales/zh-CN/skills/reverse-engineer/SKILL.md +3 -0
  72. package/bundled/locales/zh-CN/skills/reverse-engineer/tdd-analysis.md +13 -23
  73. package/bundled/locales/zh-CN/skills/runbook-assistant/SKILL.md +1 -0
  74. package/bundled/locales/zh-CN/skills/slo-assistant/SKILL.md +1 -0
  75. package/bundled/locales/zh-CN/skills/tdd-assistant/SKILL.md +2 -0
  76. package/bundled/locales/zh-CN/skills/workflows/README.md +2 -11
  77. package/bundled/locales/zh-TW/CHANGELOG.md +65 -3
  78. package/bundled/locales/zh-TW/CLAUDE.md +1 -1
  79. package/bundled/locales/zh-TW/README.md +7 -7
  80. package/bundled/locales/zh-TW/SECURITY.md +1 -1
  81. package/bundled/locales/zh-TW/core/acceptance-criteria-traceability.md +2 -0
  82. package/bundled/locales/zh-TW/core/adr-standards.md +26 -5
  83. package/bundled/locales/zh-TW/core/ai-instruction-standards.md +10 -8
  84. package/bundled/locales/zh-TW/core/code-review-checklist.md +2 -0
  85. package/bundled/locales/zh-TW/core/container-image-standards.md +2 -2
  86. package/bundled/locales/zh-TW/core/contract-testing-standards.md +2 -2
  87. package/bundled/locales/zh-TW/core/cross-flow-regression.md +8 -7
  88. package/bundled/locales/zh-TW/core/data-contract.md +2 -2
  89. package/bundled/locales/zh-TW/core/data-migration-testing.md +2 -2
  90. package/bundled/locales/zh-TW/core/data-pipeline.md +2 -2
  91. package/bundled/locales/zh-TW/core/deferred-item-exit.md +251 -0
  92. package/bundled/locales/zh-TW/core/documentation-writing-standards.md +228 -3
  93. package/bundled/locales/zh-TW/core/full-coverage-testing.md +15 -2
  94. package/bundled/locales/zh-TW/core/governance-layer.md +118 -5
  95. package/bundled/locales/zh-TW/core/iac-design-principles.md +2 -2
  96. package/bundled/locales/zh-TW/core/incident-response.md +2 -2
  97. package/bundled/locales/zh-TW/core/model-provenance.md +4 -2
  98. package/bundled/locales/zh-TW/core/pii-classification.md +42 -6
  99. package/bundled/locales/zh-TW/core/prd-standards.md +4 -2
  100. package/bundled/locales/zh-TW/core/product-metrics-standards.md +4 -2
  101. package/bundled/locales/zh-TW/core/release-readiness-gate.md +2 -2
  102. package/bundled/locales/zh-TW/core/resource-cost-boundary.md +2 -2
  103. package/bundled/locales/zh-TW/core/retrospective-standards.md +5 -3
  104. package/bundled/locales/zh-TW/core/reverse-engineering-standards.md +83 -5
  105. package/bundled/locales/zh-TW/core/runbook.md +2 -2
  106. package/bundled/locales/zh-TW/core/schema-evolution.md +2 -2
  107. package/bundled/locales/zh-TW/core/secret-management-standards.md +2 -2
  108. package/bundled/locales/zh-TW/core/slo-sli.md +2 -2
  109. package/bundled/locales/zh-TW/core/spec-driven-development.md +2 -0
  110. package/bundled/locales/zh-TW/core/tech-debt-standards.md +71 -4
  111. package/bundled/locales/zh-TW/core/turn-completion-integrity.md +190 -0
  112. package/bundled/locales/zh-TW/core/user-journey-testing.md +2 -2
  113. package/bundled/locales/zh-TW/core/user-story-mapping.md +2 -2
  114. package/bundled/locales/zh-TW/core/verification-oracle.md +2 -2
  115. package/bundled/locales/zh-TW/docs/CHEATSHEET.md +27 -6
  116. package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +29 -68
  117. package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +172 -24
  118. package/bundled/locales/zh-TW/docs/USAGE-MODES-COMPARISON.md +1 -2
  119. package/bundled/locales/zh-TW/integrations/google-antigravity/{INSTRUCTIONS.md → AGENTS.md} +1 -1
  120. package/bundled/locales/zh-TW/integrations/google-antigravity/README.md +3 -3
  121. package/bundled/locales/zh-TW/skills/adr-assistant/SKILL.md +1 -1
  122. package/bundled/locales/zh-TW/skills/agents/README.md +1 -1
  123. package/bundled/locales/zh-TW/skills/atdd-assistant/SKILL.md +2 -0
  124. package/bundled/locales/zh-TW/skills/bdd-assistant/SKILL.md +2 -0
  125. package/bundled/locales/zh-TW/skills/brainstorm-assistant/SKILL.md +22 -12
  126. package/bundled/locales/zh-TW/skills/brainstorm-assistant/guide.md +12 -9
  127. package/bundled/locales/zh-TW/skills/code-review-assistant/SKILL.md +1 -0
  128. package/bundled/locales/zh-TW/skills/commands/brainstorm.md +17 -13
  129. package/bundled/locales/zh-TW/skills/commands/config.md +0 -1
  130. package/bundled/locales/zh-TW/skills/commands/init.md +1 -2
  131. package/bundled/locales/zh-TW/skills/commit-standards/SKILL.md +2 -0
  132. package/bundled/locales/zh-TW/skills/contract-test-assistant/SKILL.md +2 -1
  133. package/bundled/locales/zh-TW/skills/dev-methodology/SKILL.md +2 -0
  134. package/bundled/locales/zh-TW/skills/dev-workflow-guide/SKILL.md +1 -1
  135. package/bundled/locales/zh-TW/skills/knowledge-graph/guide.md +2 -2
  136. package/bundled/locales/zh-TW/skills/migration-assistant/SKILL.md +1 -1
  137. package/bundled/locales/zh-TW/skills/observability-assistant/SKILL.md +1 -0
  138. package/bundled/locales/zh-TW/skills/project-discovery/SKILL.md +1 -0
  139. package/bundled/locales/zh-TW/skills/project-structure-guide/SKILL.md +1 -0
  140. package/bundled/locales/zh-TW/skills/release-standards/SKILL.md +3 -0
  141. package/bundled/locales/zh-TW/skills/requirement-assistant/SKILL.md +2 -0
  142. package/bundled/locales/zh-TW/skills/reverse-engineer/SKILL.md +3 -0
  143. package/bundled/locales/zh-TW/skills/reverse-engineer/tdd-analysis.md +13 -23
  144. package/bundled/locales/zh-TW/skills/runbook-assistant/SKILL.md +1 -0
  145. package/bundled/locales/zh-TW/skills/slo-assistant/SKILL.md +1 -0
  146. package/bundled/locales/zh-TW/skills/tdd-assistant/SKILL.md +2 -0
  147. package/bundled/locales/zh-TW/skills/workflows/README.md +2 -11
  148. package/bundled/skills/agents/README.md +1 -1
  149. package/bundled/skills/atdd-assistant/SKILL.md +2 -0
  150. package/bundled/skills/bdd-assistant/SKILL.md +2 -0
  151. package/bundled/skills/brainstorm-assistant/SKILL.md +31 -13
  152. package/bundled/skills/brainstorm-assistant/guide.md +9 -6
  153. package/bundled/skills/code-review-assistant/SKILL.md +1 -0
  154. package/bundled/skills/commands/brainstorm.md +12 -9
  155. package/bundled/skills/commands/config.md +0 -1
  156. package/bundled/skills/commands/init.md +2 -3
  157. package/bundled/skills/commit-standards/SKILL.md +2 -0
  158. package/bundled/skills/contract-test-assistant/SKILL.md +1 -0
  159. package/bundled/skills/dev-methodology/SKILL.md +4 -0
  160. package/bundled/skills/observability-assistant/SKILL.md +1 -0
  161. package/bundled/skills/project-discovery/SKILL.md +1 -0
  162. package/bundled/skills/project-structure-guide/SKILL.md +1 -0
  163. package/bundled/skills/release-standards/SKILL.md +3 -0
  164. package/bundled/skills/requirement-assistant/SKILL.md +2 -0
  165. package/bundled/skills/reverse-engineer/SKILL.md +3 -0
  166. package/bundled/skills/reverse-engineer/tdd-analysis.md +16 -23
  167. package/bundled/skills/runbook-assistant/SKILL.md +1 -0
  168. package/bundled/skills/slo-assistant/SKILL.md +1 -0
  169. package/bundled/skills/tdd-assistant/SKILL.md +2 -0
  170. package/bundled/skills/workflows/README.md +2 -11
  171. package/bundled/templates/.ai-context.yaml.template +194 -0
  172. package/bundled/templates/CLAUDE.md.template +145 -0
  173. package/bundled/templates/DESIGN.md +237 -0
  174. package/bundled/templates/SKILL-BRIEF-TEMPLATE.md +57 -0
  175. package/bundled/templates/SKILL-CANDIDATES.md +39 -0
  176. package/bundled/templates/gates/check-error-exit.mjs +309 -0
  177. package/bundled/templates/mcp-config.json +10 -0
  178. package/bundled/templates/methodology-template.yaml +209 -0
  179. package/bundled/templates/migration-template.md +408 -0
  180. package/bundled/templates/requirement-checklist.md +410 -0
  181. package/bundled/templates/requirement-document-template.md +591 -0
  182. package/bundled/templates/requirement-template.md +881 -0
  183. package/bundled/templates/reverse-spec-template.md +409 -0
  184. package/bundled/templates/test-case-template.md +74 -0
  185. package/bundled/templates/test-plan-template.md +74 -0
  186. package/package.json +9 -5
  187. package/src/commands/audit.js +82 -0
  188. package/src/commands/check.js +66 -10
  189. package/src/commands/init.js +161 -16
  190. package/src/commands/lint.js +96 -0
  191. package/src/commands/quickstart.js +16 -13
  192. package/src/commands/update.js +286 -14
  193. package/src/compilers/claude-code-compiler.js +4 -1
  194. package/src/config/ai-agent-paths.js +62 -17
  195. package/src/core/constants.js +42 -11
  196. package/src/core/manifest.js +201 -3
  197. package/src/core/paths.js +2 -2
  198. package/src/i18n/messages.js +9 -32
  199. package/src/installers/hooks-installer.js +167 -75
  200. package/src/installers/integration-installer.js +9 -5
  201. package/src/prompts/init.js +14 -14
  202. package/src/reconciler/actual-state-scanner.js +14 -3
  203. package/src/utils/detector.js +21 -1
  204. package/src/utils/effect-boundary.js +1093 -0
  205. package/src/utils/hasher.js +229 -9
  206. package/src/utils/hook-stats.js +1 -1
  207. package/src/utils/integration-generator.js +100 -4
  208. package/src/utils/reference-sync.js +4 -1
  209. package/src/utils/skills-installer.js +17 -3
  210. package/src/utils/spec-linter.js +35 -76
  211. package/src/utils/yaml-generator.js +51 -9
  212. package/standards-registry.json +31 -8
  213. package/src/commands/sync.js +0 -133
@@ -1,22 +1,76 @@
1
1
  import { createHash } from 'crypto';
2
- import { readFileSync, statSync, existsSync, readdirSync } from 'fs';
2
+ import { readFileSync, existsSync, readdirSync } from 'fs';
3
3
  import { join, relative } from 'path';
4
4
  import { UDS_MARKERS } from '../core/constants.js';
5
5
  import { resolveIntegrationFile } from '../core/constants.js';
6
+ import { isProvenanceEstablished } from '../core/manifest.js';
7
+
8
+ // GitHub issue #155. `git config core.autocrlf true` (the common
9
+ // Windows default) rewrites LF to CRLF on checkout. The manifest's stored
10
+ // hashes are computed from the LF bytes git carries in the blob (that is what
11
+ // every non-Windows install reads), so a Windows working tree — content
12
+ // byte-for-byte what `git status` calls clean — hashed to something else
13
+ // entirely. Every `.standards/*` file, every skill/command file, and every
14
+ // CLAUDE.md/AGENTS.md UDS block came back "modified" although nothing had
15
+ // changed. Normalizing line endings before hashing (both when the manifest is
16
+ // written and when it is compared) makes the hash line-ending agnostic, so
17
+ // LF and CRLF checkouts of the same content agree.
18
+ //
19
+ // This intentionally makes "someone converted this file's line endings, text
20
+ // otherwise identical" invisible to `uds check`/`uds update`. That is the
21
+ // point, not a gap: line-ending convention is a checkout artifact, not
22
+ // content a standards library should track as a modification — it is the
23
+ // same normalization `git diff`/`git status` already apply before deciding a
24
+ // tracked file is dirty.
6
25
 
7
26
  /**
8
- * Compute SHA-256 hash for a file
27
+ * Heuristic binary-content detector, matching the approach git itself uses:
28
+ * a NUL byte anywhere in the first 8000 bytes marks the buffer as binary.
29
+ * Binary content must never be line-ending-normalized — flipping `\r\n` bytes
30
+ * inside an image or font would silently corrupt the hash rather than make it
31
+ * platform-agnostic, and could hide (or manufacture) a real difference.
32
+ * Every file `uds` currently manages under `.standards/`, skills, and
33
+ * commands is text (`.md`/`.yaml`/`.json`), but this function is the general
34
+ * per-file entry point (also used for arbitrary skill directory contents),
35
+ * so the check stays in place for whatever gets added later.
36
+ * @param {Buffer} buffer
37
+ * @returns {boolean} True if the buffer looks binary
38
+ */
39
+ export function isBinaryContent(buffer) {
40
+ const sampleSize = Math.min(buffer.length, 8000);
41
+ for (let i = 0; i < sampleSize; i++) {
42
+ if (buffer[i] === 0) return true;
43
+ }
44
+ return false;
45
+ }
46
+
47
+ /**
48
+ * Normalize CRLF and lone-CR line endings to LF.
49
+ * @param {string} text
50
+ * @returns {string} Normalized text
51
+ */
52
+ export function normalizeLineEndings(text) {
53
+ return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
54
+ }
55
+
56
+ /**
57
+ * Compute SHA-256 hash for a file, normalizing line endings first (unless the
58
+ * file is binary — see `isBinaryContent`). `size` is the byte length of the
59
+ * normalized content, not the raw on-disk size, so the two stay consistent
60
+ * with each other for the quick-reject check in `compareFileHash`.
9
61
  * @param {string} filePath - Absolute file path
10
62
  * @returns {Object|null} { hash, size } or null if file doesn't exist
11
63
  */
12
64
  export function computeFileHash(filePath) {
13
65
  try {
14
- const content = readFileSync(filePath);
66
+ const raw = readFileSync(filePath);
67
+ const content = isBinaryContent(raw)
68
+ ? raw
69
+ : Buffer.from(normalizeLineEndings(raw.toString('utf-8')), 'utf-8');
15
70
  const hash = createHash('sha256').update(content).digest('hex');
16
- const stats = statSync(filePath);
17
71
  return {
18
72
  hash: `sha256:${hash}`,
19
- size: stats.size
73
+ size: content.length
20
74
  };
21
75
  } catch {
22
76
  return null;
@@ -187,6 +241,15 @@ function scanDirectory(dirPath, basePath) {
187
241
 
188
242
  /**
189
243
  * Scan for untracked files in .standards/ and integration locations
244
+ *
245
+ * ⚠ "Untracked" here means only "absent from `manifest.fileHashes`". It is NOT
246
+ * an ownership predicate, and must not be used to decide what may be deleted —
247
+ * that is what it was used for, and it is why a user's hand-written file was
248
+ * removed without warning (issue #168). Use `planStandardsRemovals` /
249
+ * `classifyFileOwnership`, which separate "did UDS write this" from "is this
250
+ * still shipped". This function survives for reporting callers that genuinely
251
+ * want the hash-table question. (XSPEC-384 R1)
252
+ *
190
253
  * @param {string} projectPath - Project root path
191
254
  * @param {Object} manifest - Manifest object
192
255
  * @returns {string[]} Array of relative paths to untracked files
@@ -218,7 +281,7 @@ export function scanForUntrackedFiles(projectPath, manifest) {
218
281
  '.clinerules',
219
282
  '.github/copilot-instructions.md',
220
283
  'CLAUDE.md',
221
- 'INSTRUCTIONS.md'
284
+ '.agents/AGENTS.md'
222
285
  ];
223
286
 
224
287
  for (const intFile of knownIntegrations) {
@@ -231,6 +294,161 @@ export function scanForUntrackedFiles(projectPath, manifest) {
231
294
  return untracked;
232
295
  }
233
296
 
297
+ /**
298
+ * The three states a file under `.standards/` can be in. (XSPEC-384 R1)
299
+ *
300
+ * `scanForUntrackedFiles` answers a two-state question — in `fileHashes` or
301
+ * not — and the caller then treated "not" as "not ours, delete it". The world
302
+ * has a third state: files UDS did write whose manifest record was lost. The
303
+ * old predicate folded that into the same branch as a user's own file, and the
304
+ * branch deletes.
305
+ */
306
+ export const FILE_OWNERSHIP = {
307
+ /** UDS wrote this file; we may remove it when it stops being shipped. */
308
+ UDS: 'uds-owned',
309
+ /** Provenance is complete and does not name this file: it is not ours. */
310
+ FOREIGN: 'foreign',
311
+ /** No provenance yet — we cannot tell, so we must not act destructively. */
312
+ UNKNOWN: 'unknown'
313
+ };
314
+
315
+ /** Paths that are never candidates for anything, with the reason. */
316
+ const STRUCTURAL_EXCLUSIONS = new Map([
317
+ ['.standards/manifest.json', 'the manifest itself']
318
+ ]);
319
+
320
+ /**
321
+ * Decide who owns one file under `.standards/`.
322
+ *
323
+ * @param {string} relPath - Path relative to project root (forward slashes)
324
+ * @param {Object} manifest - Manifest object
325
+ * @returns {string} One of FILE_OWNERSHIP
326
+ */
327
+ export function classifyFileOwnership(relPath, manifest) {
328
+ const normalized = (relPath || '').replace(/\\/g, '/');
329
+ const provenanceFiles = manifest?.provenance?.files;
330
+ if (provenanceFiles && Object.prototype.hasOwnProperty.call(provenanceFiles, normalized)) {
331
+ return FILE_OWNERSHIP.UDS;
332
+ }
333
+ // Pre-provenance evidence. A path in `fileHashes` got there because UDS put
334
+ // it there, so it is still proof of authorship — just weaker proof, since it
335
+ // is also the table that loses entries. Reading it here (rather than only
336
+ // reading provenance) is what keeps the first upgrade from disowning every
337
+ // file installed before provenance existed.
338
+ if (manifest?.fileHashes && Object.prototype.hasOwnProperty.call(manifest.fileHashes, normalized)) {
339
+ return FILE_OWNERSHIP.UDS;
340
+ }
341
+ // No record either way. Whether that means "not ours" depends entirely on
342
+ // whether our records are complete yet.
343
+ return isProvenanceEstablished(manifest) ? FILE_OWNERSHIP.FOREIGN : FILE_OWNERSHIP.UNKNOWN;
344
+ }
345
+
346
+ /**
347
+ * Walk `.standards/` and classify everything in it.
348
+ *
349
+ * A walk, not a list of expected names: `.standards/` is an open set — UDS's
350
+ * own docs invite teams to add project-specific files to it — so any
351
+ * enumeration of "files we know about" is stale the moment someone adds one,
352
+ * and being absent from that enumeration is precisely what used to get a file
353
+ * deleted. (XSPEC-384 R1)
354
+ *
355
+ * @param {string} projectPath - Project root path
356
+ * @param {Object} manifest - Manifest object
357
+ * @returns {{scanned:number, excluded:Array, udsOwned:string[], foreign:string[], unknown:string[]}}
358
+ */
359
+ export function classifyStandardsFiles(projectPath, manifest) {
360
+ const result = { scanned: 0, excluded: [], udsOwned: [], foreign: [], unknown: [] };
361
+
362
+ const standardsDir = join(projectPath, '.standards');
363
+ if (!existsSync(standardsDir)) return result;
364
+
365
+ for (const relPath of scanDirectory(standardsDir, projectPath)) {
366
+ const normalized = relPath.replace(/\\/g, '/');
367
+ result.scanned++;
368
+
369
+ const exclusionReason = STRUCTURAL_EXCLUSIONS.get(normalized);
370
+ if (exclusionReason) {
371
+ result.excluded.push({ path: normalized, reason: exclusionReason });
372
+ continue;
373
+ }
374
+
375
+ switch (classifyFileOwnership(normalized, manifest)) {
376
+ case FILE_OWNERSHIP.UDS:
377
+ result.udsOwned.push(normalized);
378
+ break;
379
+ case FILE_OWNERSHIP.FOREIGN:
380
+ result.foreign.push(normalized);
381
+ break;
382
+ default:
383
+ result.unknown.push(normalized);
384
+ }
385
+ }
386
+
387
+ return result;
388
+ }
389
+
390
+ /**
391
+ * Decide which files under `.standards/` `uds update` may delete.
392
+ *
393
+ * Deletion now needs two independent facts to line up, one per axis:
394
+ *
395
+ * 1. UDS wrote the file (ownership — provenance, or a legacy `fileHashes`
396
+ * entry as weaker evidence of the same thing)
397
+ * 2. UDS no longer ships it (currency — absence from `desiredFiles`, which
398
+ * the caller derives from the registry it just resolved)
399
+ *
400
+ * Every other combination is kept, and says why it was kept. The old rule
401
+ * collapsed both axes onto `fileHashes` membership, so it deleted files it had
402
+ * never written (#168) while retaining files it no longer shipped (#165).
403
+ *
404
+ * Nothing is removed when `desiredFiles` is null: a caller that cannot say what
405
+ * should exist has not established fact 2, and "I don't know" must not resolve
406
+ * to the destructive branch — that was the original defect.
407
+ *
408
+ * @param {string} projectPath - Project root path
409
+ * @param {Object} manifest - Manifest object
410
+ * @param {Set<string>|null} desiredFiles - Files the current registry says should exist
411
+ * @returns {{scanned:number, excluded:Array, remove:Array, keep:Array, census:Object}}
412
+ */
413
+ export function planStandardsRemovals(projectPath, manifest, desiredFiles = null) {
414
+ const census = classifyStandardsFiles(projectPath, manifest);
415
+ const remove = [];
416
+ const keep = [];
417
+
418
+ const desiredKnown = desiredFiles instanceof Set;
419
+
420
+ for (const path of census.udsOwned) {
421
+ if (!desiredKnown) {
422
+ keep.push({ path, reason: 'UDS-owned, but this run could not determine what is still shipped' });
423
+ } else if (desiredFiles.has(path)) {
424
+ keep.push({ path, reason: 'UDS-owned and still shipped' });
425
+ } else {
426
+ remove.push({ path, reason: 'UDS-owned and no longer shipped by the registry' });
427
+ }
428
+ }
429
+
430
+ for (const path of census.foreign) {
431
+ keep.push({ path, reason: 'not written by UDS' });
432
+ }
433
+
434
+ for (const path of census.unknown) {
435
+ keep.push({ path, reason: 'ownership unknown (manifest predates provenance)' });
436
+ }
437
+
438
+ return {
439
+ scanned: census.scanned,
440
+ excluded: census.excluded,
441
+ remove,
442
+ keep,
443
+ census: {
444
+ udsOwned: census.udsOwned.length,
445
+ foreign: census.foreign.length,
446
+ unknown: census.unknown.length,
447
+ desired: desiredKnown ? desiredFiles.size : null
448
+ }
449
+ };
450
+ }
451
+
234
452
  /**
235
453
  * Detect file format based on file path
236
454
  * @param {string} filePath - File path
@@ -276,7 +494,10 @@ function extractBlockContent(content, format) {
276
494
  */
277
495
  export function computeIntegrationBlockHash(filePath) {
278
496
  try {
279
- const content = readFileSync(filePath, 'utf-8');
497
+ // CLAUDE.md / AGENTS.md are always text — no binary check needed here,
498
+ // unlike `computeFileHash`.
499
+ const rawContent = readFileSync(filePath, 'utf-8');
500
+ const content = normalizeLineEndings(rawContent);
280
501
  const format = detectFormat(filePath);
281
502
  const { blockContent } = extractBlockContent(content, format);
282
503
 
@@ -287,13 +508,12 @@ export function computeIntegrationBlockHash(filePath) {
287
508
 
288
509
  const blockHash = createHash('sha256').update(blockContent).digest('hex');
289
510
  const fullHash = createHash('sha256').update(content).digest('hex');
290
- const stats = statSync(filePath);
291
511
 
292
512
  return {
293
513
  blockHash: `sha256:${blockHash}`,
294
514
  blockSize: Buffer.byteLength(blockContent, 'utf-8'),
295
515
  fullHash: `sha256:${fullHash}`,
296
- fullSize: stats.size
516
+ fullSize: Buffer.byteLength(content, 'utf-8')
297
517
  };
298
518
  } catch {
299
519
  return null;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Hook Statistics - Context-aware loading learning loop
3
3
  *
4
- * Records and analyzes trigger statistics from inject-standards.js hook.
4
+ * Records and analyzes trigger statistics from inject-standards.mjs hook.
5
5
  * Privacy: never records full prompt content or file paths.
6
6
  *
7
7
  * @module utils/hook-stats
@@ -32,7 +32,10 @@ export function resolveContentModeForTool(tool, userContentMode) {
32
32
  ? { contentMode: 'minimal', level: 3 }
33
33
  : { contentMode: 'index', level: 2 };
34
34
  case 'partial':
35
- return { contentMode: 'full', level: 1 };
35
+ // Was 'full'. Every partial-tier tool was being auto-assigned a mode
36
+ // that generated exactly the same bytes as 'index', so this is a rename
37
+ // of what these tools already got, not a change to it. (XSPEC-357 R7)
38
+ return { contentMode: 'index', level: 1 };
36
39
  case 'preview':
37
40
  return { contentMode: 'index', level: 2 };
38
41
  case 'minimal':
@@ -70,6 +73,31 @@ function getToolFileName(tool) {
70
73
  if (KNOWN_TOOL_FILES.has(tool) || /\.(md|yaml|yml|json)$/i.test(tool)) {
71
74
  return tool;
72
75
  }
76
+
77
+ // 🔴 A tool THIS system already knows, missing from THIS table, is an internal
78
+ // inconsistency — never a future tool. It must fail rather than get a generated
79
+ // filename.
80
+ //
81
+ // Measured 2026-09-08: `roo-code` is the key used by `ai-agent-paths.js`,
82
+ // `REGISTRY.json`, `agent-adapter.js` and `agents-installer.js`, while
83
+ // `SUPPORTED_AI_TOOLS` said `roo`. Every lookup here missed and fell through, so a
84
+ // real `uds init` in a Roo Code repo wrote its instructions to `roo-code.md` — a
85
+ // filename Roo Code's docs never mention. Nothing failed. Nothing warned. Same
86
+ // class as `.codex/skills/`, which this repo's own comments call "a directory UDS
87
+ // invented"; the line below was a *rule* for inventing them.
88
+ //
89
+ // ⚠️ The fallback itself is KEPT, because it is a deliberate design decision, not
90
+ // an accident: three tests assert it as "forward compatibility with future tools".
91
+ // Narrowing beats reversing — a genuinely unknown name still gets `<name>.md`; only
92
+ // the self-contradiction fails.
93
+ if (getAgentConfig(normalizedName)) {
94
+ throw new Error(
95
+ `getToolFileName: "${tool}" is a known agent in ai-agent-paths.js but is missing ` +
96
+ 'from SUPPORTED_AI_TOOLS in cli/src/core/constants.js. Add it with the file that ' +
97
+ `tool actually reads. Refusing to invent "${tool}.md" — that is how a Roo Code ` +
98
+ 'repo got its instructions written to a file Roo Code never reads.'
99
+ );
100
+ }
73
101
  return `${tool}.md`;
74
102
  }
75
103
 
@@ -2398,6 +2426,52 @@ All responses should be in **Traditional Chinese (繁體中文)**, with technica
2398
2426
  }
2399
2427
  };
2400
2428
 
2429
+ /**
2430
+ * Generate the index disclosure that opens every UDS-managed block.
2431
+ *
2432
+ * XSPEC-357 R7 — the block an adopter receives is an index. In no content mode
2433
+ * does it carry rule bodies, and inlining them is not an option: 143 `.ai.yaml`
2434
+ * files come to roughly 248k tokens. What can be fixed is the block pretending
2435
+ * otherwise. A file that lists 72 standard paths under a heading reading
2436
+ * "Standards Compliance Instructions" reads, to an agent, like the compliance
2437
+ * instructions — and Codex was measured on 2026-07-23 doing exactly that:
2438
+ * it enumerated the standards and opened none of them.
2439
+ *
2440
+ * This wording was added to the universal `AGENTS.md` summary on 2026-08-18
2441
+ * (`generateAgentsMdSummary`). That fixed one of the two producers. The other
2442
+ * one is this — and the split is worse than it sounds, because the two are
2443
+ * mutually exclusive for the same filename: selecting codex or opencode turns
2444
+ * the universal summary OFF and routes `AGENTS.md` through here instead. The
2445
+ * disclosure written for Codex was therefore in the only file a Codex adopter
2446
+ * never receives. Measured 2026-08-20 before this change: 8 distinct adopter
2447
+ * files across 3 content modes, 0 carrying the disclosure.
2448
+ *
2449
+ * @param {string} format - Output format: 'markdown' or 'plaintext'
2450
+ * @param {string} language - Language: 'en', 'zh-tw', 'zh-cn' or 'bilingual'
2451
+ * @returns {string} Disclosure paragraph, already format-adjusted
2452
+ */
2453
+ export function generateIndexDisclosure(format, language = 'en') {
2454
+ const lines = language === 'en'
2455
+ ? [
2456
+ '**This block is an index, not the standards.** The rules are NOT reproduced here.',
2457
+ 'Before acting on anything below, open the relevant file under `.standards/` and',
2458
+ 'follow its contents. Working from this block alone means working without the',
2459
+ 'standards.'
2460
+ ]
2461
+ : [
2462
+ '**這個區塊是索引,不是標準本文。** 規則並未複製於此。',
2463
+ '在依照下方任何一項行動之前,請打開 `.standards/` 底下對應的檔案並遵守其內容。',
2464
+ '只憑這個區塊工作,等同於沒有採用標準。'
2465
+ ];
2466
+
2467
+ if (format === 'markdown') {
2468
+ return lines.map((l) => `> ${l}`).join('\n');
2469
+ }
2470
+ // Plaintext targets (.cursorrules / .clinerules / .windsurfrules) render
2471
+ // neither blockquotes nor backticks, so both are stripped rather than shown.
2472
+ return lines.map((l) => l.replace(/\*\*/g, '').replace(/`/g, '')).join('\n');
2473
+ }
2474
+
2401
2475
  /**
2402
2476
  * Generate minimal standards reference for minimal content mode
2403
2477
  * @param {string[]} installedStandards - List of installed standard file paths
@@ -2733,6 +2807,10 @@ export function generateIntegrationContent(config) {
2733
2807
  }
2734
2808
 
2735
2809
  if (installedStandards.length > 0) {
2810
+ // XSPEC-357 R7 — every mode, every tool. Placed first because it is a
2811
+ // precondition for reading the rest, not a footnote to it.
2812
+ standardsContent = generateIndexDisclosure(format, language) + '\n\n' + standardsContent;
2813
+
2736
2814
  if (contentMode === 'minimal') {
2737
2815
  // Minimal mode: simple reference list
2738
2816
  standardsContent += generateMinimalStandardsReference(
@@ -3181,8 +3259,8 @@ export function parseStandardsIndexCount(content) {
3181
3259
  export function wrapWithMarkers(content, format) {
3182
3260
  const markers = UDS_MARKERS[format] || UDS_MARKERS.markdown;
3183
3261
  const warning = format === 'plaintext'
3184
- ? '# WARNING: This block is managed by UDS (universal-dev-standards). DO NOT manually edit. Use \'npx uds install\' or \'npx uds update\' to modify.'
3185
- : '<!-- WARNING: This block is managed by UDS (universal-dev-standards). DO NOT manually edit. Use \'npx uds install\' or \'npx uds update\' to modify. -->';
3262
+ ? '# WARNING: This block is managed by UDS (universal-dev-standards). DO NOT manually edit. Use \'npx uds init\' or \'npx uds update\' to modify.'
3263
+ : '<!-- WARNING: This block is managed by UDS (universal-dev-standards). DO NOT manually edit. Use \'npx uds init\' or \'npx uds update\' to modify. -->';
3186
3264
  // 冪等:warning 位於 markers **內部**,而 extractMarkedContent 取出的內容也含它,
3187
3265
  // 於是重新包裝會疊出第二份(dev-platform CLAUDE.md 實測 178/179 兩行完全相同)。
3188
3266
  // 這裡先剝掉內容開頭既有的 warning,不論上游哪條路徑造成都能修掉。
@@ -3362,7 +3440,25 @@ export function generateAgentsMdSummary(config = {}) {
3362
3440
  lines.push('# AGENTS.md');
3363
3441
  lines.push('');
3364
3442
  lines.push('> Auto-generated by [Universal Dev Standards (UDS)](https://github.com/AsiaOstrich/universal-dev-standards).');
3365
- lines.push('> Full standards available in the `.standards/` directory.');
3443
+ lines.push('>');
3444
+ // Say it as an instruction, and say what this file is NOT.
3445
+ //
3446
+ // The previous line — "Full standards available in the `.standards/` directory" — was a
3447
+ // description, and a description asks for nothing. Measured 2026-07-23: Codex read this
3448
+ // file, listed the 65 standards it indexes, and opened none of them, so the rules had the
3449
+ // same effect as not installing UDS at all. Measured again 2026-08-18 on a fresh
3450
+ // `uds init -y`: the generated file is 5,667 bytes containing 69 filename references and
3451
+ // **zero rule statements**. That is not a bug in the generator — 143 `.ai.yaml` files come
3452
+ // to roughly 248k tokens, so inlining them is not possible — which is exactly why the file
3453
+ // has to be explicit that it is an index and that the rules are elsewhere.
3454
+ //
3455
+ // This does not prove the rules get read; only XSPEC-357's P7 probe can measure that, and
3456
+ // it is not built yet. It removes the one thing that was certainly wrong: a file that read
3457
+ // as though it carried the standards when it carried their filenames. (XSPEC-357 R7)
3458
+ lines.push('> **This file is an index, not the standards.** The rules are NOT reproduced here.');
3459
+ lines.push('> Before acting on anything below, open the relevant file under `.standards/`');
3460
+ lines.push('> and follow its contents. Working from this summary alone means working');
3461
+ lines.push('> without the standards.');
3366
3462
  lines.push('');
3367
3463
 
3368
3464
  // Build & Test Commands (auto-detect project type)
@@ -296,7 +296,10 @@ export function getToolFromPath(integrationPath) {
296
296
  '.windsurfrules': 'windsurf',
297
297
  '.clinerules': 'cline',
298
298
  '.github/copilot-instructions.md': 'copilot',
299
- 'INSTRUCTIONS.md': 'antigravity',
299
+ // Antigravity never read INSTRUCTIONS.md.
300
+ // Measured 2026-09-08 with two positive controls in the same run: tokens planted in `AGENTS.md` and `.agents/AGENTS.md` both came back with correct attribution; the one in INSTRUCTIONS.md did not.
301
+ // `.agents/AGENTS.md` is used rather than the repo root so it does not collide with Codex/OpenCode, which both target root AGENTS.md.
302
+ '.agents/AGENTS.md': 'antigravity',
300
303
  'CLAUDE.md': 'claude-code',
301
304
  '.standards/CLAUDE.md': 'claude-code',
302
305
  'AGENTS.md': 'codex'
@@ -19,7 +19,7 @@ import {
19
19
  getCommandsSupportedAgents,
20
20
  getCommandFileExtension
21
21
  } from '../config/ai-agent-paths.js';
22
- import { computeDirectoryHashes, computeFileHash } from './hasher.js';
22
+ import { computeDirectoryHashes, computeFileHash, normalizeLineEndings } from './hasher.js';
23
23
  import { isLocalizedLocale } from './locale.js';
24
24
  import { getSkillsSourceDir } from './skills-source.js';
25
25
 
@@ -504,6 +504,16 @@ export function resolveSkillFiles(skillName, locale = 'en') {
504
504
  * are different installs, and a hash over contents alone would call a rename
505
505
  * "unchanged".
506
506
  *
507
+ * Content is line-ending normalized before hashing (GitHub issue #155): the
508
+ * counterpart on the actual-state side, `hashInstalledSkillDir` in
509
+ * `reconciler/actual-state-scanner.js`, hashes files that were checked out
510
+ * into the adopter's own repo and so may have been rewritten to CRLF by
511
+ * `core.autocrlf=true` on Windows. Both sides must apply the same
512
+ * normalization or every skill would report as changed on Windows, same as
513
+ * the `.standards/*` hashes this issue was filed about — the two functions
514
+ * only stay comparable by construction if they agree on this too, which is
515
+ * why the paired tests in `skill-content-hash.test.js` exist.
516
+ *
507
517
  * @param {Array<{name: string, content: string}>} files
508
518
  * @returns {string|null} `sha256:<hex>`, or null for an empty resolution
509
519
  */
@@ -513,7 +523,7 @@ export function computeSkillContentHash(files) {
513
523
  for (const f of files) {
514
524
  h.update(f.name);
515
525
  h.update('\0');
516
- h.update(f.content);
526
+ h.update(normalizeLineEndings(f.content));
517
527
  h.update('\0');
518
528
  }
519
529
  return `sha256:${h.digest('hex')}`;
@@ -779,13 +789,17 @@ export function resolveCommandContent(cmdName, agent, locale = 'en') {
779
789
  *
780
790
  * Same `sha256:` shape as `computeSkillContentHash` so the two sides of the
781
791
  * diff can be compared without caring which category an entry came from.
792
+ * Line-ending normalized before hashing for the same reason as
793
+ * `computeSkillContentHash` (GitHub issue #155) — its actual-state
794
+ * counterpart in `reconciler/actual-state-scanner.js` hashes an installed
795
+ * command file that may have been checked out as CRLF on Windows.
782
796
  *
783
797
  * @param {string|null} content
784
798
  * @returns {string|null}
785
799
  */
786
800
  export function computeCommandContentHash(content) {
787
801
  if (content === null || content === undefined) return null;
788
- return `sha256:${createHash('sha256').update(content).digest('hex')}`;
802
+ return `sha256:${createHash('sha256').update(normalizeLineEndings(content)).digest('hex')}`;
789
803
  }
790
804
 
791
805
  function installSingleCommand(cmdName, targetDir, agent, locale = 'en') {
@@ -2,6 +2,22 @@
2
2
  * Spec Linter — Stateless analysis functions for spec quality checks
3
3
  * @module utils/spec-linter
4
4
  * @see specs/superspec-borrowing-phase1-2-spec.md (AC-11, AC-12, AC-13)
5
+ *
6
+ * XSPEC-383 R5 (Option E): `checkACCoverage` / `collectTestFiles` / `scanDir`
7
+ * were removed here. They shipped 2026-04-07, had passing unit tests, and were
8
+ * never wired to any CLI command — `uds lint` did not exist. 2026-08-19,
9
+ * registering `uds lint` and running it against VibeOps's 93 real specs
10
+ * surfaced that the removed check would have reported 0/98 ACs covered on
11
+ * every one of them, for two independent reasons: it derived AC identifiers
12
+ * positionally (`AC-1`, `AC-2`, …) instead of reading the ones a spec
13
+ * declares, and it hardcoded the `@AC-N` tag convention from this repo's own
14
+ * `skills/ac-coverage`, while VibeOps's tests tag coverage as `AC-045-001`
15
+ * without an `@` prefix. Neither project is "wrong" — they never agreed on a
16
+ * convention — but a linter that always reports zero regardless of actual
17
+ * coverage is worse than no linter: it looks like a working gate. Redoing AC
18
+ * coverage requires deciding how identifiers are read and how conventions are
19
+ * negotiated across adopters; that is a new design, not a patch, and is out
20
+ * of scope for R5.
5
21
  */
6
22
 
7
23
  import { readFileSync, readdirSync, existsSync } from 'fs';
@@ -9,35 +25,6 @@ import { join, basename } from 'path';
9
25
  import { StandardValidator } from './standard-validator.js';
10
26
  import { MicroSpec } from '../vibe/micro-spec.js';
11
27
 
12
- /**
13
- * Check if a spec's ACs are referenced in test files
14
- * @param {string} specId - e.g. "SPEC-001"
15
- * @param {string[]} acIds - e.g. ["AC-1", "AC-2", "AC-3"]
16
- * @param {string} projectPath - Project root directory
17
- * @returns {{ covered: string[], orphans: string[], coverage: number }}
18
- */
19
- export function checkACCoverage(specId, acIds, projectPath) {
20
- const covered = [];
21
- const orphans = [];
22
-
23
- // Collect all test file contents
24
- const testContent = collectTestFiles(projectPath);
25
-
26
- for (const acId of acIds) {
27
- // Search for @AC-N pattern in test files
28
- const pattern = new RegExp(`@${acId}\\b`);
29
- if (testContent.some(content => pattern.test(content))) {
30
- covered.push(acId);
31
- } else {
32
- orphans.push(acId);
33
- }
34
- }
35
-
36
- const coverage = acIds.length > 0 ? covered.length / acIds.length : 0;
37
-
38
- return { covered, orphans, coverage };
39
- }
40
-
41
28
  /**
42
29
  * Validate depends_on references exist
43
30
  * @param {Object[]} specs - Array of specs with { id, dependsOn }
@@ -75,14 +62,22 @@ export function checkSpecSize(specFilePath, options = {}) {
75
62
  }
76
63
 
77
64
  /**
78
- * Run all lint checks on specs in a project
65
+ * Run all lint checks (dependency validity + size) on specs in a project.
66
+ *
67
+ * `specsDirExists` is a three-state signal, not decoration: "no specs
68
+ * directory" and "specs directory with zero problems" must not collapse into
69
+ * the same `{ pass: 0, warn: 0, fail: 0 }` shape, or a caller cannot tell
70
+ * "nothing was checked" from "everything is fine" (XSPEC-383 R4/R5's own
71
+ * standing rule for this repo's gates — see check-module-reachability.mjs and
72
+ * check-command-existence.mjs).
73
+ *
79
74
  * @param {string} projectPath - Project root directory
80
- * @returns {{ results: Object[], summary: { pass: number, warn: number, fail: number } }}
75
+ * @returns {{ results: Object[], summary: { pass: number, warn: number, fail: number }, specsDir: string, specsDirExists: boolean }}
81
76
  */
82
77
  export function lintAll(projectPath) {
83
78
  const specsDir = join(projectPath, 'specs');
84
79
  if (!existsSync(specsDir)) {
85
- return { results: [], summary: { pass: 0, warn: 0, fail: 0 } };
80
+ return { results: [], summary: { pass: 0, warn: 0, fail: 0 }, specsDir: 'specs', specsDirExists: false };
86
81
  }
87
82
 
88
83
  // Load all specs
@@ -102,10 +97,6 @@ export function lintAll(projectPath) {
102
97
  const summary = { pass: 0, warn: 0, fail: 0 };
103
98
 
104
99
  for (const spec of allSpecs) {
105
- // AC coverage
106
- const acIds = (spec.acceptance || []).map((_, i) => `AC-${i + 1}`);
107
- const acCoverage = checkACCoverage(spec.id, acIds, projectPath);
108
-
109
100
  // Dependencies for this spec
110
101
  const specBroken = depResults.broken.filter(b => b.spec === spec.id);
111
102
  const specValid = depResults.valid.filter(v => v.spec === spec.id);
@@ -116,22 +107,18 @@ export function lintAll(projectPath) {
116
107
  const size = checkSpecSize(specPath);
117
108
 
118
109
  // Determine worst status
119
- let worstStatus = 'pass';
120
- if (specBroken.length > 0 || size.status === 'fail' || acCoverage.coverage === 0 && acIds.length > 0) {
121
- worstStatus = 'fail';
122
- } else if (size.status === 'warn' || acCoverage.coverage < 1) {
123
- worstStatus = 'warn';
110
+ let status = 'pass';
111
+ if (specBroken.length > 0 || size.status === 'fail') {
112
+ status = 'fail';
113
+ } else if (size.status === 'warn') {
114
+ status = 'warn';
124
115
  }
125
116
 
126
- summary[worstStatus]++;
117
+ summary[status]++;
127
118
 
128
119
  results.push({
129
120
  spec: spec.id,
130
- acCoverage: {
131
- covered: acCoverage.covered,
132
- orphans: acCoverage.orphans,
133
- coverage: acCoverage.coverage,
134
- },
121
+ status,
135
122
  deps,
136
123
  size: {
137
124
  effectiveLines: size.effectiveLines,
@@ -140,33 +127,5 @@ export function lintAll(projectPath) {
140
127
  });
141
128
  }
142
129
 
143
- return { results, summary };
144
- }
145
-
146
- // ─── Internal helpers ───
147
-
148
- function collectTestFiles(projectPath) {
149
- const contents = [];
150
- const testDirs = ['tests', 'test', '__tests__', 'cli/tests'];
151
-
152
- for (const dir of testDirs) {
153
- const fullDir = join(projectPath, dir);
154
- if (existsSync(fullDir)) {
155
- scanDir(fullDir, contents);
156
- }
157
- }
158
-
159
- return contents;
160
- }
161
-
162
- function scanDir(dir, contents) {
163
- const entries = readdirSync(dir, { withFileTypes: true });
164
- for (const entry of entries) {
165
- const fullPath = join(dir, entry.name);
166
- if (entry.isDirectory()) {
167
- scanDir(fullPath, contents);
168
- } else if (entry.name.match(/\.(test|spec)\.(js|ts|mjs|cjs)$/)) {
169
- contents.push(readFileSync(fullPath, 'utf-8'));
170
- }
171
- }
130
+ return { results, summary, specsDir: 'specs', specsDirExists: true };
172
131
  }