universal-dev-standards 6.7.5 → 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.
- package/bin/uds.js +7 -0
- package/bundled/ai/standards/ai-instruction-standards.ai.yaml +6 -6
- package/bundled/core/ai-instruction-standards.md +9 -7
- package/bundled/locales/zh-CN/CHANGELOG.md +21 -3
- package/bundled/locales/zh-CN/README.md +6 -6
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/core/ai-instruction-standards.md +10 -8
- package/bundled/locales/zh-CN/docs/CHEATSHEET.md +20 -6
- package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +149 -11
- package/bundled/locales/zh-CN/skills/agents/README.md +1 -1
- package/bundled/locales/zh-CN/skills/reverse-engineer/tdd-analysis.md +13 -23
- package/bundled/locales/zh-CN/skills/workflows/README.md +2 -11
- package/bundled/locales/zh-TW/CHANGELOG.md +21 -3
- package/bundled/locales/zh-TW/README.md +6 -6
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/ai-instruction-standards.md +10 -8
- package/bundled/locales/zh-TW/docs/CHEATSHEET.md +20 -6
- package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +149 -11
- package/bundled/locales/zh-TW/skills/agents/README.md +1 -1
- package/bundled/locales/zh-TW/skills/reverse-engineer/tdd-analysis.md +13 -23
- package/bundled/locales/zh-TW/skills/workflows/README.md +2 -11
- package/bundled/skills/agents/README.md +1 -1
- package/bundled/skills/reverse-engineer/tdd-analysis.md +16 -23
- package/bundled/skills/workflows/README.md +2 -11
- package/package.json +4 -2
- package/src/commands/lint.js +96 -0
- package/src/commands/quickstart.js +16 -13
- package/src/i18n/messages.js +3 -3
- package/src/reconciler/actual-state-scanner.js +14 -3
- package/src/utils/hasher.js +63 -8
- package/src/utils/integration-generator.js +21 -3
- package/src/utils/skills-installer.js +17 -3
- package/src/utils/spec-linter.js +35 -76
- package/standards-registry.json +7 -7
- package/src/commands/sync.js +0 -133
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `uds lint` — dependency validity + size checks against installed specs
|
|
3
|
+
* (specs/*.md).
|
|
4
|
+
*
|
|
5
|
+
* XSPEC-383 R5 (Option E). `cli/src/utils/spec-linter.js` and this command's
|
|
6
|
+
* JSON shape existed since 2026-04-07, but no `uds lint` command was ever
|
|
7
|
+
* registered — VibeOps's `lint-executor.ts` has been calling
|
|
8
|
+
* `npx uds lint --json` since the same day and getting `command not found`
|
|
9
|
+
* every time, four and a half months, without anyone noticing (see
|
|
10
|
+
* `cli/scripts/check-module-reachability.mjs` for the full incident).
|
|
11
|
+
*
|
|
12
|
+
* This registers the command with exactly the two checks that survived
|
|
13
|
+
* `lintAll()`'s AC-coverage removal (see spec-linter.js for why AC coverage
|
|
14
|
+
* was dropped rather than patched). The `--json` shape is deliberately NOT a
|
|
15
|
+
* new design — it matches the shape VibeOps's `lint-executor.ts` has already
|
|
16
|
+
* been parsing (`result.summary.fail`, `result.results[].specId/.status/.message`)
|
|
17
|
+
* since it was written, so wiring this up does not also require a change on
|
|
18
|
+
* the VibeOps side.
|
|
19
|
+
*
|
|
20
|
+
* @module commands/lint
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import chalk from 'chalk';
|
|
24
|
+
import { lintAll } from '../utils/spec-linter.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Render a one-line human message for a single spec's lint result.
|
|
28
|
+
* Exported for tests; also used to build the `message` field of `--json`
|
|
29
|
+
* output.
|
|
30
|
+
*/
|
|
31
|
+
export function buildMessage(result) {
|
|
32
|
+
const parts = [];
|
|
33
|
+
if (result.deps.broken.length > 0) {
|
|
34
|
+
const targets = result.deps.broken.map((b) => b.target).join(', ');
|
|
35
|
+
parts.push(
|
|
36
|
+
`${result.deps.broken.length} broken dependenc${result.deps.broken.length === 1 ? 'y' : 'ies'}: ${targets}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
parts.push(`${result.size.effectiveLines} effective lines (${result.size.status})`);
|
|
40
|
+
return parts.join('; ');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function lintCommand(options = {}) {
|
|
44
|
+
const projectPath = process.cwd();
|
|
45
|
+
const result = lintAll(projectPath);
|
|
46
|
+
|
|
47
|
+
if (options.json) {
|
|
48
|
+
const payload = {
|
|
49
|
+
summary: result.summary,
|
|
50
|
+
results: result.results.map((r) => ({
|
|
51
|
+
specId: r.spec,
|
|
52
|
+
status: r.status,
|
|
53
|
+
message: buildMessage(r),
|
|
54
|
+
})),
|
|
55
|
+
};
|
|
56
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
57
|
+
if (result.summary.fail > 0) process.exitCode = 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log();
|
|
62
|
+
console.log(chalk.bold('Spec Lint'));
|
|
63
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
64
|
+
|
|
65
|
+
if (!result.specsDirExists) {
|
|
66
|
+
// 查無 spec 目錄 must say so explicitly — an empty { pass: 0, warn: 0,
|
|
67
|
+
// fail: 0 } summary is indistinguishable from "checked, all clean" unless
|
|
68
|
+
// the command says out loud that nothing was scanned.
|
|
69
|
+
console.log(chalk.yellow(` 查無 spec 目錄(./${result.specsDir}/ 不存在)`));
|
|
70
|
+
console.log(chalk.gray(' 沒有東西被掃描——這不代表沒有問題,代表沒有檢查。'));
|
|
71
|
+
console.log();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(chalk.gray(` 掃描 ${result.results.length} 份 spec(./${result.specsDir}/)`));
|
|
76
|
+
console.log();
|
|
77
|
+
|
|
78
|
+
for (const r of result.results) {
|
|
79
|
+
const msg = buildMessage(r);
|
|
80
|
+
if (r.status === 'fail') {
|
|
81
|
+
console.log(chalk.red(` ✗ ${r.spec}: ${msg}`));
|
|
82
|
+
} else if (r.status === 'warn') {
|
|
83
|
+
console.log(chalk.yellow(` ⚠ ${r.spec}: ${msg}`));
|
|
84
|
+
} else {
|
|
85
|
+
console.log(chalk.green(` ✓ ${r.spec}: ${msg}`));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log();
|
|
90
|
+
console.log(
|
|
91
|
+
chalk.gray(` ${result.summary.pass} pass, ${result.summary.warn} warn, ${result.summary.fail} fail`)
|
|
92
|
+
);
|
|
93
|
+
console.log();
|
|
94
|
+
|
|
95
|
+
if (result.summary.fail > 0) process.exitCode = 1;
|
|
96
|
+
}
|
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Reduces cognitive load by guiding users to common workflow paths.
|
|
5
5
|
*
|
|
6
|
+
* ⚠️ Every `cmd` that starts with `uds ` MUST be a command this CLI actually
|
|
7
|
+
* registers, with flags it actually accepts. This is the one place whose entire
|
|
8
|
+
* job is "find the right commands quickly", so a wrong entry here does the exact
|
|
9
|
+
* opposite of what the command exists for.
|
|
10
|
+
*
|
|
11
|
+
* 2026-08-19: four entries pointed at things that do not exist — `uds lint`
|
|
12
|
+
* (twice), `uds sync` (twice, one of them the sole content of a whole
|
|
13
|
+
* workflow), `uds check --spec-size`, and `uds spec create --boost`. Verify with
|
|
14
|
+
* `uds <cmd> --definitely-not-a-real-flag` and look for "unknown command";
|
|
15
|
+
* `uds <cmd> --help` is NOT a valid check — commander prints the general help
|
|
16
|
+
* for an unknown command instead of erroring, so --help reports every name as
|
|
17
|
+
* valid.
|
|
18
|
+
*
|
|
6
19
|
* @module commands/quickstart
|
|
7
20
|
*/
|
|
8
21
|
|
|
@@ -24,11 +37,10 @@ export const WORKFLOWS = [
|
|
|
24
37
|
name: 'Full SDD Spec Flow (Boost)',
|
|
25
38
|
description: 'Complete spec-driven development for complex features',
|
|
26
39
|
steps: [
|
|
27
|
-
{ cmd: '
|
|
28
|
-
{ cmd: 'uds
|
|
40
|
+
{ cmd: '# Use the /sdd skill', desc: 'Full spec lifecycle with review (see `uds spec --help`)' },
|
|
41
|
+
{ cmd: 'uds spec create "your feature" --scope fullstack', desc: 'Create the spec, scoped' },
|
|
29
42
|
{ cmd: 'uds spec confirm SPEC-XXX', desc: 'Confirm after review' },
|
|
30
43
|
{ cmd: '# Implement with /derive → /tdd', desc: 'Use forward derivation and TDD' },
|
|
31
|
-
{ cmd: 'uds sync', desc: 'Export context for session resume' },
|
|
32
44
|
],
|
|
33
45
|
},
|
|
34
46
|
{
|
|
@@ -46,19 +58,10 @@ export const WORKFLOWS = [
|
|
|
46
58
|
description: 'Audit standards compliance and spec quality',
|
|
47
59
|
steps: [
|
|
48
60
|
{ cmd: 'uds check', desc: 'Check standards file integrity' },
|
|
49
|
-
{ cmd: 'uds check --
|
|
50
|
-
{ cmd: 'uds lint', desc: 'Lint specs for AC coverage and dependency validity' },
|
|
61
|
+
{ cmd: 'uds check --i18n', desc: 'Run i18n lint rules across canonical + locale variants' },
|
|
51
62
|
{ cmd: 'uds audit', desc: 'Deep health diagnosis' },
|
|
52
63
|
],
|
|
53
64
|
},
|
|
54
|
-
{
|
|
55
|
-
name: 'Resume Previous Work',
|
|
56
|
-
description: 'Restore context from a previous session',
|
|
57
|
-
steps: [
|
|
58
|
-
{ cmd: 'uds sync', desc: 'Generate context.md from git diff + workflow state' },
|
|
59
|
-
{ cmd: 'cat .workflow-state/context.md', desc: 'Read context in new session' },
|
|
60
|
-
],
|
|
61
|
-
},
|
|
62
65
|
];
|
|
63
66
|
|
|
64
67
|
/**
|
package/src/i18n/messages.js
CHANGED
|
@@ -757,7 +757,7 @@ export const messages = {
|
|
|
757
757
|
actionsAvailable: 'Actions available:',
|
|
758
758
|
restoreOption: '• Run `uds check --restore` to restore all modified/missing files',
|
|
759
759
|
diffOption: '• Run `uds check --diff` to view changes',
|
|
760
|
-
interactiveOption: '• Run `uds check
|
|
760
|
+
interactiveOption: '• Run `uds check` for file-by-file decisions (interactive by default)',
|
|
761
761
|
// Interactive mode
|
|
762
762
|
interactiveMode: 'Interactive Mode:',
|
|
763
763
|
filesNeedAttention: '{count} file(s) need attention.',
|
|
@@ -1991,7 +1991,7 @@ export const messages = {
|
|
|
1991
1991
|
actionsAvailable: '可用操作:',
|
|
1992
1992
|
restoreOption: '• 執行 `uds check --restore` 還原所有已修改/遺失的檔案',
|
|
1993
1993
|
diffOption: '• 執行 `uds check --diff` 檢視變更',
|
|
1994
|
-
interactiveOption: '• 執行 `uds check
|
|
1994
|
+
interactiveOption: '• 執行 `uds check` 逐一處理檔案(預設為互動模式)',
|
|
1995
1995
|
// Interactive mode
|
|
1996
1996
|
interactiveMode: '互動模式:',
|
|
1997
1997
|
filesNeedAttention: '{count} 個檔案需要注意。',
|
|
@@ -3240,7 +3240,7 @@ export const messages = {
|
|
|
3240
3240
|
actionsAvailable: '可用操作:',
|
|
3241
3241
|
restoreOption: '• 执行 `uds check --restore` 恢复所有已修改/缺失文件',
|
|
3242
3242
|
diffOption: '• 执行 `uds check --diff` 查看更改',
|
|
3243
|
-
interactiveOption: '• 执行 `uds check
|
|
3243
|
+
interactiveOption: '• 执行 `uds check` 逐文件决策(默认为交互模式)',
|
|
3244
3244
|
// Interactive mode
|
|
3245
3245
|
interactiveMode: '交互模式:',
|
|
3246
3246
|
filesNeedAttention: '{count} 个文件需要关注。',
|
|
@@ -11,7 +11,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
|
11
11
|
import { createHash } from 'crypto';
|
|
12
12
|
import { join, relative } from 'path';
|
|
13
13
|
import { readManifest } from '../core/manifest.js';
|
|
14
|
-
import { computeFileHash, computeIntegrationBlockHash } from '../utils/hasher.js';
|
|
14
|
+
import { computeFileHash, computeIntegrationBlockHash, normalizeLineEndings } from '../utils/hasher.js';
|
|
15
15
|
import { SUPPORTED_AI_TOOLS, UDS_MARKERS } from '../core/constants.js';
|
|
16
16
|
import { getSkillsDirForAgent, getCommandsDirForAgent, getCommandFileExtension } from '../config/ai-agent-paths.js';
|
|
17
17
|
import { getSkillsSourceEntryNames, getAvailableCommandNames } from '../utils/skills-installer.js';
|
|
@@ -322,6 +322,13 @@ function shippedCommandNames() {
|
|
|
322
322
|
* materialising a second array first. The format is the contract; if it changes
|
|
323
323
|
* in one place and not the other every skill reports as changed, which the
|
|
324
324
|
* paired tests in `skill-content-hash.test.js` exist to catch.
|
|
325
|
+
*
|
|
326
|
+
* Content is line-ending normalized before hashing (GitHub issue #155): these
|
|
327
|
+
* are files installed into the adopter's own project, which `git checkout`
|
|
328
|
+
* may have rewritten to CRLF under `core.autocrlf=true` on Windows even
|
|
329
|
+
* though the desired side (`computeSkillContentHash`, reading UDS's own
|
|
330
|
+
* package source) never sees a `\r`. Without matching normalization here,
|
|
331
|
+
* every skill would report as changed on every Windows `uds update`.
|
|
325
332
|
*/
|
|
326
333
|
function hashInstalledSkillDir(dirPath) {
|
|
327
334
|
let entries;
|
|
@@ -346,7 +353,7 @@ function hashInstalledSkillDir(dirPath) {
|
|
|
346
353
|
}
|
|
347
354
|
h.update(name);
|
|
348
355
|
h.update('\0');
|
|
349
|
-
h.update(content);
|
|
356
|
+
h.update(normalizeLineEndings(content));
|
|
350
357
|
h.update('\0');
|
|
351
358
|
}
|
|
352
359
|
return `sha256:${h.digest('hex')}`;
|
|
@@ -437,12 +444,16 @@ function scanCommands(state, projectPath, manifest) {
|
|
|
437
444
|
// Content hash of the installed file, for UDS-managed commands only —
|
|
438
445
|
// same reasoning as skills: an adopter's own command has no desired
|
|
439
446
|
// counterpart to compare against. (XSPEC-382 R7)
|
|
447
|
+
//
|
|
448
|
+
// Line-ending normalized before hashing (GitHub issue #155), matching
|
|
449
|
+
// `computeCommandContentHash` on the desired side — the same CRLF
|
|
450
|
+
// checkout risk `hashInstalledSkillDir` above documents applies here.
|
|
440
451
|
const cmdUdsManaged = shippedCommandNames().has(cmdName);
|
|
441
452
|
let cmdHash = null;
|
|
442
453
|
if (cmdUdsManaged) {
|
|
443
454
|
try {
|
|
444
455
|
cmdHash = `sha256:${createHash('sha256')
|
|
445
|
-
.update(readFileSync(join(cmdsDir, entry.name), 'utf-8'))
|
|
456
|
+
.update(normalizeLineEndings(readFileSync(join(cmdsDir, entry.name), 'utf-8')))
|
|
446
457
|
.digest('hex')}`;
|
|
447
458
|
} catch {
|
|
448
459
|
// Unreadable → no hash. A partial answer would claim a match that
|
package/src/utils/hasher.js
CHANGED
|
@@ -1,22 +1,75 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
-
import { readFileSync,
|
|
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
6
|
|
|
7
|
+
// GitHub issue #155. `git config core.autocrlf true` (the common
|
|
8
|
+
// Windows default) rewrites LF to CRLF on checkout. The manifest's stored
|
|
9
|
+
// hashes are computed from the LF bytes git carries in the blob (that is what
|
|
10
|
+
// every non-Windows install reads), so a Windows working tree — content
|
|
11
|
+
// byte-for-byte what `git status` calls clean — hashed to something else
|
|
12
|
+
// entirely. Every `.standards/*` file, every skill/command file, and every
|
|
13
|
+
// CLAUDE.md/AGENTS.md UDS block came back "modified" although nothing had
|
|
14
|
+
// changed. Normalizing line endings before hashing (both when the manifest is
|
|
15
|
+
// written and when it is compared) makes the hash line-ending agnostic, so
|
|
16
|
+
// LF and CRLF checkouts of the same content agree.
|
|
17
|
+
//
|
|
18
|
+
// This intentionally makes "someone converted this file's line endings, text
|
|
19
|
+
// otherwise identical" invisible to `uds check`/`uds update`. That is the
|
|
20
|
+
// point, not a gap: line-ending convention is a checkout artifact, not
|
|
21
|
+
// content a standards library should track as a modification — it is the
|
|
22
|
+
// same normalization `git diff`/`git status` already apply before deciding a
|
|
23
|
+
// tracked file is dirty.
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Heuristic binary-content detector, matching the approach git itself uses:
|
|
27
|
+
* a NUL byte anywhere in the first 8000 bytes marks the buffer as binary.
|
|
28
|
+
* Binary content must never be line-ending-normalized — flipping `\r\n` bytes
|
|
29
|
+
* inside an image or font would silently corrupt the hash rather than make it
|
|
30
|
+
* platform-agnostic, and could hide (or manufacture) a real difference.
|
|
31
|
+
* Every file `uds` currently manages under `.standards/`, skills, and
|
|
32
|
+
* commands is text (`.md`/`.yaml`/`.json`), but this function is the general
|
|
33
|
+
* per-file entry point (also used for arbitrary skill directory contents),
|
|
34
|
+
* so the check stays in place for whatever gets added later.
|
|
35
|
+
* @param {Buffer} buffer
|
|
36
|
+
* @returns {boolean} True if the buffer looks binary
|
|
37
|
+
*/
|
|
38
|
+
export function isBinaryContent(buffer) {
|
|
39
|
+
const sampleSize = Math.min(buffer.length, 8000);
|
|
40
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
41
|
+
if (buffer[i] === 0) return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Normalize CRLF and lone-CR line endings to LF.
|
|
48
|
+
* @param {string} text
|
|
49
|
+
* @returns {string} Normalized text
|
|
50
|
+
*/
|
|
51
|
+
export function normalizeLineEndings(text) {
|
|
52
|
+
return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
7
55
|
/**
|
|
8
|
-
* Compute SHA-256 hash for a file
|
|
56
|
+
* Compute SHA-256 hash for a file, normalizing line endings first (unless the
|
|
57
|
+
* file is binary — see `isBinaryContent`). `size` is the byte length of the
|
|
58
|
+
* normalized content, not the raw on-disk size, so the two stay consistent
|
|
59
|
+
* with each other for the quick-reject check in `compareFileHash`.
|
|
9
60
|
* @param {string} filePath - Absolute file path
|
|
10
61
|
* @returns {Object|null} { hash, size } or null if file doesn't exist
|
|
11
62
|
*/
|
|
12
63
|
export function computeFileHash(filePath) {
|
|
13
64
|
try {
|
|
14
|
-
const
|
|
65
|
+
const raw = readFileSync(filePath);
|
|
66
|
+
const content = isBinaryContent(raw)
|
|
67
|
+
? raw
|
|
68
|
+
: Buffer.from(normalizeLineEndings(raw.toString('utf-8')), 'utf-8');
|
|
15
69
|
const hash = createHash('sha256').update(content).digest('hex');
|
|
16
|
-
const stats = statSync(filePath);
|
|
17
70
|
return {
|
|
18
71
|
hash: `sha256:${hash}`,
|
|
19
|
-
size:
|
|
72
|
+
size: content.length
|
|
20
73
|
};
|
|
21
74
|
} catch {
|
|
22
75
|
return null;
|
|
@@ -276,7 +329,10 @@ function extractBlockContent(content, format) {
|
|
|
276
329
|
*/
|
|
277
330
|
export function computeIntegrationBlockHash(filePath) {
|
|
278
331
|
try {
|
|
279
|
-
|
|
332
|
+
// CLAUDE.md / AGENTS.md are always text — no binary check needed here,
|
|
333
|
+
// unlike `computeFileHash`.
|
|
334
|
+
const rawContent = readFileSync(filePath, 'utf-8');
|
|
335
|
+
const content = normalizeLineEndings(rawContent);
|
|
280
336
|
const format = detectFormat(filePath);
|
|
281
337
|
const { blockContent } = extractBlockContent(content, format);
|
|
282
338
|
|
|
@@ -287,13 +343,12 @@ export function computeIntegrationBlockHash(filePath) {
|
|
|
287
343
|
|
|
288
344
|
const blockHash = createHash('sha256').update(blockContent).digest('hex');
|
|
289
345
|
const fullHash = createHash('sha256').update(content).digest('hex');
|
|
290
|
-
const stats = statSync(filePath);
|
|
291
346
|
|
|
292
347
|
return {
|
|
293
348
|
blockHash: `sha256:${blockHash}`,
|
|
294
349
|
blockSize: Buffer.byteLength(blockContent, 'utf-8'),
|
|
295
350
|
fullHash: `sha256:${fullHash}`,
|
|
296
|
-
fullSize:
|
|
351
|
+
fullSize: Buffer.byteLength(content, 'utf-8')
|
|
297
352
|
};
|
|
298
353
|
} catch {
|
|
299
354
|
return null;
|
|
@@ -3181,8 +3181,8 @@ export function parseStandardsIndexCount(content) {
|
|
|
3181
3181
|
export function wrapWithMarkers(content, format) {
|
|
3182
3182
|
const markers = UDS_MARKERS[format] || UDS_MARKERS.markdown;
|
|
3183
3183
|
const warning = format === 'plaintext'
|
|
3184
|
-
? '# WARNING: This block is managed by UDS (universal-dev-standards). DO NOT manually edit. Use \'npx uds
|
|
3185
|
-
: '<!-- WARNING: This block is managed by UDS (universal-dev-standards). DO NOT manually edit. Use \'npx uds
|
|
3184
|
+
? '# WARNING: This block is managed by UDS (universal-dev-standards). DO NOT manually edit. Use \'npx uds init\' or \'npx uds update\' to modify.'
|
|
3185
|
+
: '<!-- 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
3186
|
// 冪等:warning 位於 markers **內部**,而 extractMarkedContent 取出的內容也含它,
|
|
3187
3187
|
// 於是重新包裝會疊出第二份(dev-platform CLAUDE.md 實測 178/179 兩行完全相同)。
|
|
3188
3188
|
// 這裡先剝掉內容開頭既有的 warning,不論上游哪條路徑造成都能修掉。
|
|
@@ -3362,7 +3362,25 @@ export function generateAgentsMdSummary(config = {}) {
|
|
|
3362
3362
|
lines.push('# AGENTS.md');
|
|
3363
3363
|
lines.push('');
|
|
3364
3364
|
lines.push('> Auto-generated by [Universal Dev Standards (UDS)](https://github.com/AsiaOstrich/universal-dev-standards).');
|
|
3365
|
-
lines.push('>
|
|
3365
|
+
lines.push('>');
|
|
3366
|
+
// Say it as an instruction, and say what this file is NOT.
|
|
3367
|
+
//
|
|
3368
|
+
// The previous line — "Full standards available in the `.standards/` directory" — was a
|
|
3369
|
+
// description, and a description asks for nothing. Measured 2026-07-23: Codex read this
|
|
3370
|
+
// file, listed the 65 standards it indexes, and opened none of them, so the rules had the
|
|
3371
|
+
// same effect as not installing UDS at all. Measured again 2026-08-18 on a fresh
|
|
3372
|
+
// `uds init -y`: the generated file is 5,667 bytes containing 69 filename references and
|
|
3373
|
+
// **zero rule statements**. That is not a bug in the generator — 143 `.ai.yaml` files come
|
|
3374
|
+
// to roughly 248k tokens, so inlining them is not possible — which is exactly why the file
|
|
3375
|
+
// has to be explicit that it is an index and that the rules are elsewhere.
|
|
3376
|
+
//
|
|
3377
|
+
// This does not prove the rules get read; only XSPEC-357's P7 probe can measure that, and
|
|
3378
|
+
// it is not built yet. It removes the one thing that was certainly wrong: a file that read
|
|
3379
|
+
// as though it carried the standards when it carried their filenames. (XSPEC-357 R7)
|
|
3380
|
+
lines.push('> **This file is an index, not the standards.** The rules are NOT reproduced here.');
|
|
3381
|
+
lines.push('> Before acting on anything below, open the relevant file under `.standards/`');
|
|
3382
|
+
lines.push('> and follow its contents. Working from this summary alone means working');
|
|
3383
|
+
lines.push('> without the standards.');
|
|
3366
3384
|
lines.push('');
|
|
3367
3385
|
|
|
3368
3386
|
// Build & Test Commands (auto-detect project type)
|
|
@@ -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') {
|
package/src/utils/spec-linter.js
CHANGED
|
@@ -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
|
|
120
|
-
if (specBroken.length > 0 || size.status === 'fail'
|
|
121
|
-
|
|
122
|
-
} else if (size.status === 'warn'
|
|
123
|
-
|
|
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[
|
|
117
|
+
summary[status]++;
|
|
127
118
|
|
|
128
119
|
results.push({
|
|
129
120
|
spec: spec.id,
|
|
130
|
-
|
|
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
|
}
|
package/standards-registry.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"version": "6.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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"
|