universal-dev-standards 6.7.4 → 6.8.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 (45) hide show
  1. package/bin/uds.js +7 -0
  2. package/bundled/ai/standards/ai-instruction-standards.ai.yaml +6 -6
  3. package/bundled/core/ai-instruction-standards.md +9 -7
  4. package/bundled/locales/zh-CN/CHANGELOG.md +32 -3
  5. package/bundled/locales/zh-CN/README.md +6 -6
  6. package/bundled/locales/zh-CN/SECURITY.md +1 -1
  7. package/bundled/locales/zh-CN/core/ai-instruction-standards.md +10 -8
  8. package/bundled/locales/zh-CN/docs/CHEATSHEET.md +20 -6
  9. package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +149 -11
  10. package/bundled/locales/zh-CN/skills/agents/README.md +1 -1
  11. package/bundled/locales/zh-CN/skills/reverse-engineer/tdd-analysis.md +13 -23
  12. package/bundled/locales/zh-CN/skills/spec-derivation/SKILL.md +5 -1
  13. package/bundled/locales/zh-CN/skills/workflows/README.md +2 -11
  14. package/bundled/locales/zh-TW/CHANGELOG.md +32 -3
  15. package/bundled/locales/zh-TW/README.md +6 -6
  16. package/bundled/locales/zh-TW/SECURITY.md +1 -1
  17. package/bundled/locales/zh-TW/core/ai-instruction-standards.md +10 -8
  18. package/bundled/locales/zh-TW/docs/CHEATSHEET.md +20 -6
  19. package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +149 -11
  20. package/bundled/locales/zh-TW/skills/agents/README.md +1 -1
  21. package/bundled/locales/zh-TW/skills/reverse-engineer/tdd-analysis.md +13 -23
  22. package/bundled/locales/zh-TW/skills/spec-derivation/SKILL.md +5 -1
  23. package/bundled/locales/zh-TW/skills/workflows/README.md +2 -11
  24. package/bundled/skills/agents/README.md +1 -1
  25. package/bundled/skills/audit-assistant/SKILL.md +24 -1
  26. package/bundled/skills/changelog-guide/SKILL.md +24 -1
  27. package/bundled/skills/commit-standards/SKILL.md +24 -1
  28. package/bundled/skills/docs-generator/SKILL.md +24 -1
  29. package/bundled/skills/project-discovery/SKILL.md +24 -1
  30. package/bundled/skills/release-standards/SKILL.md +24 -1
  31. package/bundled/skills/reverse-engineer/SKILL.md +24 -1
  32. package/bundled/skills/reverse-engineer/tdd-analysis.md +16 -23
  33. package/bundled/skills/spec-derivation/SKILL.md +24 -1
  34. package/bundled/skills/workflows/README.md +2 -11
  35. package/package.json +4 -2
  36. package/src/commands/lint.js +96 -0
  37. package/src/commands/quickstart.js +16 -13
  38. package/src/i18n/messages.js +3 -3
  39. package/src/reconciler/actual-state-scanner.js +14 -3
  40. package/src/utils/hasher.js +63 -8
  41. package/src/utils/integration-generator.js +21 -3
  42. package/src/utils/skills-installer.js +17 -3
  43. package/src/utils/spec-linter.js +35 -76
  44. package/standards-registry.json +7 -7
  45. package/src/commands/sync.js +0 -133
@@ -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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "6.7.4",
3
+ "version": "6.8.0",
4
4
  "lastUpdated": "2026-05-13",
5
5
  "description": "Standards registry for universal-dev-standards with integrated skills and AI-optimized formats",
6
6
  "formats": {
@@ -58,14 +58,14 @@
58
58
  "standards": {
59
59
  "name": "universal-dev-standards",
60
60
  "url": "https://github.com/AsiaOstrich/universal-dev-standards",
61
- "version": "6.7.4"
61
+ "version": "6.8.0"
62
62
  },
63
63
  "skills": {
64
64
  "name": "universal-dev-standards",
65
65
  "url": "https://github.com/AsiaOstrich/universal-dev-standards",
66
66
  "localPath": "skills",
67
67
  "rawUrl": "https://raw.githubusercontent.com/AsiaOstrich/universal-dev-standards/main/skills",
68
- "version": "6.7.4",
68
+ "version": "6.8.0",
69
69
  "note": "Skills are now included in the main repository under skills/"
70
70
  }
71
71
  },
@@ -2260,7 +2260,7 @@
2260
2260
  "id": "license-compliance",
2261
2261
  "name": "License Compliance Standards",
2262
2262
  "nameZh": "授權合規標準",
2263
- "version": "6.7.4",
2263
+ "version": "6.8.0",
2264
2264
  "source": {
2265
2265
  "human": "core/license-compliance.md",
2266
2266
  "ai": "ai/standards/license-compliance.ai.yaml"
@@ -2272,7 +2272,7 @@
2272
2272
  "id": "verification-oracle",
2273
2273
  "name": "Verification Oracle Standards",
2274
2274
  "nameZh": "驗證 Oracle 標準",
2275
- "version": "6.7.4",
2275
+ "version": "6.8.0",
2276
2276
  "source": {
2277
2277
  "human": "core/verification-oracle.md",
2278
2278
  "ai": "ai/standards/verification-oracle.ai.yaml"
@@ -2284,7 +2284,7 @@
2284
2284
  "id": "model-provenance",
2285
2285
  "name": "Model Provenance Policy Standards",
2286
2286
  "nameZh": "模型來源政策標準",
2287
- "version": "6.7.4",
2287
+ "version": "6.8.0",
2288
2288
  "source": {
2289
2289
  "human": "core/model-provenance.md",
2290
2290
  "ai": "ai/standards/model-provenance.ai.yaml"
@@ -2296,7 +2296,7 @@
2296
2296
  "id": "resource-cost-boundary",
2297
2297
  "name": "Resource / Cost Boundary Declaration Standards",
2298
2298
  "nameZh": "資源/成本邊界宣告標準",
2299
- "version": "6.7.4",
2299
+ "version": "6.8.0",
2300
2300
  "source": {
2301
2301
  "human": "core/resource-cost-boundary.md",
2302
2302
  "ai": "ai/standards/resource-cost-boundary.ai.yaml"
@@ -1,133 +0,0 @@
1
- /**
2
- * Sync Command — Export current work context for session resume
3
- * Zero AI tokens — pure shell/JS execution
4
- *
5
- * @module commands/sync
6
- * @see specs/superspec-borrowing-phase1-2-spec.md (AC-16, AC-17)
7
- */
8
-
9
- import { execSync } from 'child_process';
10
- import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from 'fs';
11
- import { join } from 'path';
12
-
13
- const MAX_LINES = 500;
14
-
15
- /**
16
- * Generate context.md from git info and optional workflow state
17
- * @param {string} projectPath - Project root directory
18
- * @returns {string} Path to generated context.md
19
- */
20
- export function generateContext(projectPath) {
21
- const lines = [];
22
-
23
- lines.push('# Development Context');
24
- lines.push(`> Auto-generated by \`uds sync\` at ${new Date().toISOString()}`);
25
- lines.push('> This file helps restore context in a new AI session.');
26
- lines.push('');
27
-
28
- // ─── Git Status ───
29
- lines.push('## Git Status');
30
- lines.push('');
31
-
32
- const branch = gitCmd('git rev-parse --abbrev-ref HEAD', projectPath);
33
- lines.push(`- **Branch**: ${branch}`);
34
-
35
- const baseBranch = detectBaseBranch(projectPath);
36
- if (baseBranch && baseBranch !== branch) {
37
- const commitCount = gitCmd(`git rev-list --count ${baseBranch}..HEAD`, projectPath) || '0';
38
- lines.push(`- **Base**: ${baseBranch} (+${commitCount} commits)`);
39
- }
40
-
41
- lines.push('');
42
- lines.push('### Recent Commits');
43
- lines.push('');
44
-
45
- const logOutput = gitCmd('git log --oneline -20', projectPath);
46
- if (logOutput) {
47
- lines.push(...logOutput.split('\n').map(l => `- ${l}`));
48
- } else {
49
- lines.push('(no commits)');
50
- }
51
- lines.push('');
52
-
53
- lines.push('### Changed Files');
54
- lines.push('');
55
-
56
- if (baseBranch && baseBranch !== branch) {
57
- const diffStat = gitCmd(`git diff --stat ${baseBranch}..HEAD`, projectPath);
58
- if (diffStat) {
59
- lines.push('```');
60
- lines.push(...diffStat.split('\n'));
61
- lines.push('```');
62
- }
63
- } else {
64
- lines.push('(on base branch, no diff)');
65
- }
66
- lines.push('');
67
-
68
- // ─── Workflow State ───
69
- const workflowState = readWorkflowState(projectPath);
70
- if (workflowState) {
71
- lines.push('## Workflow State');
72
- lines.push('');
73
- lines.push(...workflowState);
74
- lines.push('');
75
- }
76
-
77
- // Truncate to MAX_LINES
78
- const truncated = lines.slice(0, MAX_LINES);
79
-
80
- // Write to .workflow-state/context.md
81
- const outputDir = join(projectPath, '.workflow-state');
82
- mkdirSync(outputDir, { recursive: true });
83
- const outputPath = join(outputDir, 'context.md');
84
- writeFileSync(outputPath, truncated.join('\n'), 'utf-8');
85
-
86
- return outputPath;
87
- }
88
-
89
- // ─── Helpers ───
90
-
91
- function gitCmd(cmd, cwd) {
92
- try {
93
- return execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
94
- } catch {
95
- return '';
96
- }
97
- }
98
-
99
- function detectBaseBranch(projectPath) {
100
- // Check for common base branches
101
- for (const name of ['main', 'master']) {
102
- const exists = gitCmd(`git rev-parse --verify ${name}`, projectPath);
103
- if (exists) return name;
104
- }
105
- return null;
106
- }
107
-
108
- function readWorkflowState(projectPath) {
109
- const stateDir = join(projectPath, '.workflow-state');
110
- if (!existsSync(stateDir)) return null;
111
-
112
- const yamlFiles = readdirSync(stateDir)
113
- .filter(f => f.endsWith('.yaml') && !f.endsWith('.log.yaml'))
114
- .sort();
115
-
116
- if (yamlFiles.length === 0) return null;
117
-
118
- // Read the most recent state file
119
- const latestFile = yamlFiles[yamlFiles.length - 1];
120
- const content = readFileSync(join(stateDir, latestFile), 'utf-8');
121
-
122
- const lines = [];
123
- // Extract key fields
124
- const specMatch = content.match(/spec_id:\s*(.+)/);
125
- const phaseMatch = content.match(/current_phase:\s*(.+)/);
126
- const statusMatch = content.match(/status:\s*(.+)/);
127
-
128
- if (specMatch) lines.push(`- **Spec**: ${specMatch[1].trim()}`);
129
- if (phaseMatch) lines.push(`- **Phase**: ${phaseMatch[1].trim()}`);
130
- if (statusMatch) lines.push(`- **Status**: ${statusMatch[1].trim()}`);
131
-
132
- return lines.length > 0 ? lines : null;
133
- }