universal-dev-standards 6.14.0-beta.4 → 6.14.0-beta.5
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 -1
- package/bundled/ai/standards/full-coverage-testing.ai.yaml +46 -5
- package/bundled/core/full-coverage-testing.md +57 -3
- package/bundled/locales/zh-CN/CHANGELOG.md +29 -2
- package/bundled/locales/zh-CN/README.md +1 -1
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/core/full-coverage-testing.md +61 -7
- package/bundled/locales/zh-CN/docs/CHEATSHEET.md +3 -1
- package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +8 -5
- package/bundled/locales/zh-TW/CHANGELOG.md +29 -2
- package/bundled/locales/zh-TW/README.md +1 -1
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/full-coverage-testing.md +61 -7
- package/bundled/locales/zh-TW/docs/CHEATSHEET.md +3 -1
- package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +8 -5
- package/bundled/templates/gates/check-anti-fake-tests.mjs +991 -0
- package/bundled/templates/gates/check-stubs.mjs +644 -0
- package/package.json +2 -2
- package/src/commands/audit.js +11 -0
- package/src/commands/check.js +124 -24
- package/src/commands/init.js +16 -0
- package/src/commands/update.js +180 -19
- package/src/core/install-records.js +2 -1
- package/src/i18n/messages.js +50 -9
- package/src/reconciler/backup-manager.js +418 -82
- package/src/reconciler/index.js +27 -5
- package/src/reconciler/install-roots.js +90 -0
- package/src/reconciler/plan-executor.js +23 -2
- package/src/uninstallers/hook-uninstaller.js +2 -1
- package/src/utils/command-hash-ownership.js +103 -0
- package/src/utils/copier.js +21 -1
- package/src/utils/gate-scripts.js +141 -0
- package/src/utils/health-scorer.js +10 -7
- package/src/utils/skill-hash-ownership.js +64 -0
- package/src/utils/skills-installer.js +12 -1
- package/src/utils/test-change-check.js +160 -0
- package/src/utils/test-policy.js +214 -0
- package/src/utils/update-summary.js +29 -0
- package/standards-registry.json +7 -7
- package/bundled/extensions/languages/php/fat-free-patterns.md +0 -915
- package/bundled/extensions/languages/php/php-style.md +0 -693
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "This commit changes code and touches no test" — the pre-commit check.
|
|
3
|
+
* // implements XSPEC-444 R2
|
|
4
|
+
*
|
|
5
|
+
* Runs inside `uds check`, which is what the pre-commit hook UDS writes calls. It reads
|
|
6
|
+
* what is staged (`git diff --cached`); with nothing staged (a CI run, a manual
|
|
7
|
+
* `uds check`) it says nothing, because there is no commit to judge.
|
|
8
|
+
*
|
|
9
|
+
* What it decides, per staged file (see test-policy.js for the table):
|
|
10
|
+
* source + no test in the same change → warn, listing every code file
|
|
11
|
+
* a file of a type the table has never seen → warn, listing it and saying how to
|
|
12
|
+
* classify it. Never read as "fine".
|
|
13
|
+
* pure rename (git's R100) → exempt; recorded in the output with its reason
|
|
14
|
+
* deletion → not a code change that needs a test
|
|
15
|
+
* `exempt` entry in the policy → exempt; recorded with the adopter's reason
|
|
16
|
+
*
|
|
17
|
+
* Severity: "warn" by default. `"mode": "block"` in `.standards/test-policy.json` makes a
|
|
18
|
+
* code change without a test fail the check. An unclassified file never blocks — it is
|
|
19
|
+
* UDS's own gap, not the committer's.
|
|
20
|
+
*
|
|
21
|
+
* NOT done here, on purpose (reported to the maintainer instead of guessed):
|
|
22
|
+
* - the ratchet ("count of unpaired changes may only go down"): `uds check` is read-only
|
|
23
|
+
* by design and the spec does not say where the baseline lives or what a "change" is
|
|
24
|
+
* counted in.
|
|
25
|
+
* - per-commit exemption with a reason: a pre-commit hook cannot read the commit message
|
|
26
|
+
* (that is the commit-msg hook), so a per-commit reason has nowhere to be read from.
|
|
27
|
+
* Standing exemptions with reasons are `exempt` entries in the policy file.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { execFileSync } from 'child_process';
|
|
31
|
+
import chalk from 'chalk';
|
|
32
|
+
import { loadPolicy, classifyPath, POLICY_FILE } from './test-policy.js';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse `git diff --name-status -z` output.
|
|
36
|
+
* Records: `A\0path\0`, `M\0path\0`, `D\0path\0`, `R100\0old\0new\0`, `C75\0old\0new\0`.
|
|
37
|
+
* @returns {{ status: string, path: string, oldPath?: string, similarity?: number }[]}
|
|
38
|
+
*/
|
|
39
|
+
export function parseNameStatusZ(output) {
|
|
40
|
+
const parts = output.split('\0');
|
|
41
|
+
if (parts[parts.length - 1] === '') parts.pop();
|
|
42
|
+
const out = [];
|
|
43
|
+
for (let i = 0; i < parts.length;) {
|
|
44
|
+
const code = parts[i++];
|
|
45
|
+
if (!code) continue;
|
|
46
|
+
const letter = code[0];
|
|
47
|
+
if (letter === 'R' || letter === 'C') {
|
|
48
|
+
out.push({ status: letter, oldPath: parts[i++], path: parts[i++], similarity: parseInt(code.slice(1), 10) || 0 });
|
|
49
|
+
} else {
|
|
50
|
+
out.push({ status: letter, path: parts[i++] });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The staged changes of the repository at `projectPath`, or `{ error }` when git cannot say. */
|
|
57
|
+
export function readStagedChanges(projectPath) {
|
|
58
|
+
try {
|
|
59
|
+
const out = execFileSync('git', ['-c', 'core.quotepath=off', 'diff', '--cached', '--name-status', '-M', '-z'], {
|
|
60
|
+
cwd: projectPath,
|
|
61
|
+
encoding: 'utf-8',
|
|
62
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
63
|
+
maxBuffer: 64 * 1024 * 1024
|
|
64
|
+
});
|
|
65
|
+
return { changes: parseNameStatusZ(out) };
|
|
66
|
+
} catch (e) {
|
|
67
|
+
const stderr = String(e.stderr || e.message || '').trim().split('\n')[0];
|
|
68
|
+
// "not a git repository" is not a failure of this check: there is no commit to judge.
|
|
69
|
+
if (/not a git repository/i.test(stderr) || e.code === 'ENOENT') return { notRepo: true, error: stderr || 'git not found' };
|
|
70
|
+
return { error: stderr || 'git diff failed' };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Judge a list of staged changes against a policy. Pure: no I/O.
|
|
76
|
+
* @returns {{
|
|
77
|
+
* staged: number, source: string[], tests: string[], unclassified: string[],
|
|
78
|
+
* exempt: {path:string, reason:string}[], verdict: 'none'|'ok'|'warn'
|
|
79
|
+
* }}
|
|
80
|
+
*/
|
|
81
|
+
export function judgeChanges(changes, policy) {
|
|
82
|
+
const result = { staged: changes.length, source: [], tests: [], unclassified: [], exempt: [], verdict: 'none' };
|
|
83
|
+
for (const ch of changes) {
|
|
84
|
+
if (ch.status === 'D') continue;
|
|
85
|
+
const cls = classifyPath(ch.path, policy);
|
|
86
|
+
if (cls.kind === 'test') { result.tests.push(ch.path); continue; }
|
|
87
|
+
if (cls.kind === 'unclassified') { result.unclassified.push(ch.path); continue; }
|
|
88
|
+
if (cls.kind !== 'source') continue; // ignored / noncode
|
|
89
|
+
if (ch.status === 'R' && ch.similarity === 100) {
|
|
90
|
+
result.exempt.push({ path: ch.path, reason: `pure rename of ${ch.oldPath} (content unchanged)` });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (cls.exempt) {
|
|
94
|
+
result.exempt.push({ path: ch.path, reason: `policy exemption "${cls.exempt.pattern}": ${cls.exempt.reason}` });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
result.source.push(ch.path);
|
|
98
|
+
}
|
|
99
|
+
if (result.staged === 0) result.verdict = 'none';
|
|
100
|
+
else if (result.source.length > 0 && result.tests.length === 0) result.verdict = 'warn';
|
|
101
|
+
else result.verdict = 'ok';
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const show = (list, max = 12) => {
|
|
106
|
+
const lines = list.slice(0, max).map((f) => ` - ${f}`);
|
|
107
|
+
if (list.length > max) lines.push(` ... and ${list.length - max} more`);
|
|
108
|
+
return lines;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Run the check and print its report. Never throws; never changes the exit code in
|
|
113
|
+
* "warn" mode. In "block" mode a code change with no test sets `process.exitCode = 1`.
|
|
114
|
+
* @returns {{ ran: boolean, stagedCount: number, verdict: string, mode: string, blocked: boolean, policyProblems: string[] }}
|
|
115
|
+
*/
|
|
116
|
+
export function runTestChangeCheck(projectPath, { log = console.log } = {}) {
|
|
117
|
+
const { policy, problems } = loadPolicy(projectPath);
|
|
118
|
+
const staged = readStagedChanges(projectPath);
|
|
119
|
+
const base = { ran: false, stagedCount: 0, verdict: 'none', mode: policy.mode, blocked: false, policyProblems: problems };
|
|
120
|
+
|
|
121
|
+
for (const p of problems) log(chalk.yellow(` ⚠ [test-change] ${p}`));
|
|
122
|
+
|
|
123
|
+
if (staged.notRepo) return base;
|
|
124
|
+
if (staged.error) {
|
|
125
|
+
log(chalk.yellow(` ⚠ [test-change] could not read the staged changes (${staged.error}); the code-without-test check did not run.`));
|
|
126
|
+
return base;
|
|
127
|
+
}
|
|
128
|
+
const judged = judgeChanges(staged.changes, policy);
|
|
129
|
+
const result = { ...base, ran: true, stagedCount: judged.staged, verdict: judged.verdict };
|
|
130
|
+
if (judged.staged === 0) return result;
|
|
131
|
+
|
|
132
|
+
const blocking = judged.verdict === 'warn' && policy.mode === 'block';
|
|
133
|
+
|
|
134
|
+
if (judged.verdict === 'warn') {
|
|
135
|
+
const head = blocking
|
|
136
|
+
? ` ✗ [test-change] BLOCKED: this commit changes ${judged.source.length} code file(s) and touches no test file (XSPEC-444 R2, mode "block").`
|
|
137
|
+
: ` ⚠ [test-change] This commit changes ${judged.source.length} code file(s) and touches no test file (XSPEC-444 R2). Warning only — the commit is not blocked.`;
|
|
138
|
+
log(blocking ? chalk.red(head) : chalk.yellow(head));
|
|
139
|
+
log(chalk.gray(' code files changed without a test change:'));
|
|
140
|
+
for (const l of show(judged.source)) log(chalk.gray(l));
|
|
141
|
+
log(chalk.gray(' A change to behavior needs a test that fails without it. Not a behavior change (generated code, a rename with edits)?'));
|
|
142
|
+
log(chalk.gray(` Record the exemption with its reason in ${POLICY_FILE}: "exempt": [{ "pattern": "<glob>", "reason": "<why>" }]`));
|
|
143
|
+
if (!blocking) log(chalk.gray(` To make this block the commit instead: set "mode": "block" in ${POLICY_FILE}.`));
|
|
144
|
+
}
|
|
145
|
+
if (judged.unclassified.length > 0) {
|
|
146
|
+
log(chalk.yellow(` ⚠ [test-change] ${judged.unclassified.length} changed file(s) are of a type UDS does not recognize, so it cannot tell whether they are code (never assumed fine, never blocks):`));
|
|
147
|
+
for (const l of show(judged.unclassified)) log(chalk.gray(l));
|
|
148
|
+
log(chalk.gray(` Say what they are in ${POLICY_FILE}: "sourceExtensions": ["<ext>"] (code) or "nonCodeExtensions": ["<ext>"] (not code); test files: "testPatterns".`));
|
|
149
|
+
}
|
|
150
|
+
if (judged.exempt.length > 0) {
|
|
151
|
+
log(chalk.gray(' ℹ [test-change] exempt from the test requirement, with the reason on record:'));
|
|
152
|
+
for (const e of judged.exempt.slice(0, 12)) log(chalk.gray(` - ${e.path} — ${e.reason}`));
|
|
153
|
+
}
|
|
154
|
+
if (blocking) {
|
|
155
|
+
process.exitCode = 1;
|
|
156
|
+
result.blocked = true;
|
|
157
|
+
}
|
|
158
|
+
if (judged.verdict === 'warn' || judged.unclassified.length > 0 || judged.exempt.length > 0) log();
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which files are code, which are tests, and which UDS cannot tell — one answer,
|
|
3
|
+
* configurable per project. // implements XSPEC-444 R2 (and the classification R5's scripts share)
|
|
4
|
+
*
|
|
5
|
+
* Why this is data and not a branch per language: UDS is a general standard, and the
|
|
6
|
+
* check built on this must work in every language an adopter writes. So "what is a test
|
|
7
|
+
* file" is a list of path patterns with defaults for the common ecosystems, and a project
|
|
8
|
+
* adds its own in `.standards/test-policy.json`. No test framework is named anywhere.
|
|
9
|
+
*
|
|
10
|
+
* Three answers beyond "code" and "test", and the third is the point:
|
|
11
|
+
* - `noncode` docs, data, config, images — known not to need a test
|
|
12
|
+
* - `ignored` generated/vendored/tooling paths (node_modules, dist, .github, ...)
|
|
13
|
+
* - `unclassified` an extension this table has never heard of. It is NOT assumed to be
|
|
14
|
+
* code and it is NOT assumed to be safe: the caller must say so out
|
|
15
|
+
* loud. A language UDS does not know must not read as "passed".
|
|
16
|
+
*
|
|
17
|
+
* The same defaults are copied into the two gate scripts UDS ships to adopters
|
|
18
|
+
* (templates/gates/check-anti-fake-tests.mjs, check-stubs.mjs) because those run in a
|
|
19
|
+
* project with no UDS code beside them. A test (test-policy-drift) compares all three.
|
|
20
|
+
*
|
|
21
|
+
* Policy file `.standards/test-policy.json` (optional; every list ADDS to the defaults):
|
|
22
|
+
* {
|
|
23
|
+
* "mode": "warn" | "block", // default "warn"
|
|
24
|
+
* "testDirs": ["integration"], // directory names that hold tests
|
|
25
|
+
* "testPatterns": ["*.itest.*"], // file-name globs of test files
|
|
26
|
+
* "sourceExtensions": ["zig"], // extensions that are code
|
|
27
|
+
* "nonCodeExtensions": ["gradle"], // extensions that are not
|
|
28
|
+
* "ignore": ["generated/**"], // path globs to leave alone
|
|
29
|
+
* "exempt": [{ "pattern": "src/gen/**", "reason": "generated by protoc" }]
|
|
30
|
+
* }
|
|
31
|
+
* An `exempt` entry without a `reason` is not honored (and is reported): an exemption that
|
|
32
|
+
* leaves no explanation is just a switch.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { existsSync, readFileSync } from 'fs';
|
|
36
|
+
import { join } from 'path';
|
|
37
|
+
|
|
38
|
+
// ---- BEGIN SHARED POLICY (identical in templates/gates/check-anti-fake-tests.mjs and check-stubs.mjs; test-policy-drift.test.js compares them) ----
|
|
39
|
+
/** Where an adopter's policy lives (inside `.standards/`, beside release-config.yaml). */
|
|
40
|
+
export const POLICY_FILE = '.standards/test-policy.json';
|
|
41
|
+
|
|
42
|
+
export const DEFAULT_POLICY = Object.freeze({
|
|
43
|
+
mode: 'warn',
|
|
44
|
+
testDirs: Object.freeze(['test', 'tests', '__tests__', 'spec', 'e2e']),
|
|
45
|
+
testPatterns: Object.freeze([
|
|
46
|
+
'*.test.*', '*.spec.*', '*_test.*', '*_spec.*', 'test_*.*', 'conftest.py',
|
|
47
|
+
'*Test.*', '*Tests.*', '*Spec.scala', '*Spec.kt', '*Spec.groovy'
|
|
48
|
+
]),
|
|
49
|
+
sourceExtensions: Object.freeze([
|
|
50
|
+
'js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx', 'mts', 'cts', 'vue', 'svelte', 'astro',
|
|
51
|
+
'py', 'pyi', 'java', 'kt', 'kts', 'scala', 'groovy', 'go', 'rs', 'rb', 'php', 'cs', 'fs', 'vb',
|
|
52
|
+
'swift', 'm', 'mm', 'dart', 'c', 'h', 'cc', 'cpp', 'cxx', 'hpp', 'hh',
|
|
53
|
+
'ex', 'exs', 'erl', 'hrl', 'lua', 'pl', 'pm', 'r', 'jl', 'clj', 'cljs', 'hs', 'ml', 'mli',
|
|
54
|
+
'sh', 'bash', 'zsh', 'ps1'
|
|
55
|
+
]),
|
|
56
|
+
nonCodeExtensions: Object.freeze([
|
|
57
|
+
'md', 'mdx', 'txt', 'rst', 'adoc', 'json', 'jsonc', 'json5', 'yaml', 'yml', 'toml', 'ini', 'cfg',
|
|
58
|
+
'conf', 'properties', 'env', 'lock', 'xml', 'csv', 'tsv', 'svg', 'png', 'jpg', 'jpeg', 'gif', 'ico',
|
|
59
|
+
'webp', 'avif', 'pdf', 'woff', 'woff2', 'ttf', 'otf', 'eot', 'map', 'html', 'htm', 'css', 'scss',
|
|
60
|
+
'sass', 'less', 'sql', 'gitignore', 'gitattributes', 'editorconfig', 'npmrc', 'nvmrc', 'license',
|
|
61
|
+
'log', 'snap'
|
|
62
|
+
]),
|
|
63
|
+
nonCodeNames: Object.freeze([
|
|
64
|
+
'LICENSE', 'README', 'CHANGELOG', 'NOTICE', 'AUTHORS', 'CODEOWNERS', 'Dockerfile', 'Makefile', 'Procfile'
|
|
65
|
+
]),
|
|
66
|
+
ignoreDirs: Object.freeze([
|
|
67
|
+
'node_modules', 'vendor', 'dist', 'build', 'coverage', '.git', '.standards', '.claude', '.husky',
|
|
68
|
+
'.github', 'docs', 'target', '__pycache__', '.venv', 'venv', '.next', '.nuxt', '.idea', '.vscode',
|
|
69
|
+
'.ruff_cache', '.pytest_cache', '.mypy_cache', '.tox', '.gradle', '.cache', '.turbo', '.svelte-kit',
|
|
70
|
+
'.parcel-cache', '.terraform', '.dart_tool', '.eggs'
|
|
71
|
+
]),
|
|
72
|
+
ignore: Object.freeze(['**/*.min.js', '**/*.min.css', '**/*.d.ts']),
|
|
73
|
+
exempt: Object.freeze([])
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const norm = (p) => String(p).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
77
|
+
|
|
78
|
+
/** Glob → RegExp. `*` stays inside a path segment, `**` crosses them, `?` is one non-slash character. */
|
|
79
|
+
export function globToRegex(glob) {
|
|
80
|
+
const g = norm(glob);
|
|
81
|
+
let re = '';
|
|
82
|
+
for (let i = 0; i < g.length; i++) {
|
|
83
|
+
const c = g[i];
|
|
84
|
+
if (c === '*') {
|
|
85
|
+
if (g[i + 1] === '*') {
|
|
86
|
+
if (g[i + 2] === '/') { re += '(?:.*/)?'; i += 2; } else { re += '.*'; i += 1; }
|
|
87
|
+
} else {
|
|
88
|
+
re += '[^/]*';
|
|
89
|
+
}
|
|
90
|
+
} else if (c === '?') {
|
|
91
|
+
re += '[^/]';
|
|
92
|
+
} else if ('\\^$+.()|{}[]'.includes(c)) {
|
|
93
|
+
re += '\\' + c;
|
|
94
|
+
} else {
|
|
95
|
+
re += c;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return new RegExp(`^${re}$`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A glob with no slash is a file-name glob (matches the basename anywhere); otherwise it matches the whole path. */
|
|
102
|
+
function matchesGlob(relPath, glob) {
|
|
103
|
+
const p = norm(relPath);
|
|
104
|
+
const g = norm(glob);
|
|
105
|
+
const target = g.includes('/') ? p : p.split('/').pop();
|
|
106
|
+
return globToRegex(g).test(target);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const extOf = (relPath) => {
|
|
110
|
+
const base = norm(relPath).split('/').pop();
|
|
111
|
+
const i = base.lastIndexOf('.');
|
|
112
|
+
return i > 0 ? base.slice(i + 1).toLowerCase() : '';
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Read `.standards/test-policy.json` and merge it over the defaults.
|
|
117
|
+
* @returns {{ policy: object, problems: string[], source: string|null }}
|
|
118
|
+
* `problems` is never swallowed: an unreadable file or a bad value is reported, and the
|
|
119
|
+
* defaults (mode "warn") are what runs.
|
|
120
|
+
*/
|
|
121
|
+
export function loadPolicy(projectPath) {
|
|
122
|
+
const problems = [];
|
|
123
|
+
const policy = {
|
|
124
|
+
mode: DEFAULT_POLICY.mode,
|
|
125
|
+
testDirs: [...DEFAULT_POLICY.testDirs],
|
|
126
|
+
testPatterns: [...DEFAULT_POLICY.testPatterns],
|
|
127
|
+
sourceExtensions: [...DEFAULT_POLICY.sourceExtensions],
|
|
128
|
+
nonCodeExtensions: [...DEFAULT_POLICY.nonCodeExtensions],
|
|
129
|
+
nonCodeNames: [...DEFAULT_POLICY.nonCodeNames],
|
|
130
|
+
ignoreDirs: [...DEFAULT_POLICY.ignoreDirs],
|
|
131
|
+
ignore: [...DEFAULT_POLICY.ignore],
|
|
132
|
+
exempt: []
|
|
133
|
+
};
|
|
134
|
+
const file = join(projectPath, POLICY_FILE);
|
|
135
|
+
if (!existsSync(file)) return { policy, problems, source: null };
|
|
136
|
+
|
|
137
|
+
let raw;
|
|
138
|
+
try {
|
|
139
|
+
raw = JSON.parse(readFileSync(file, 'utf-8').replace(/^\uFEFF/, ''));
|
|
140
|
+
} catch (e) {
|
|
141
|
+
problems.push(`${POLICY_FILE} cannot be read (${e.message}); defaults are in effect`);
|
|
142
|
+
return { policy, problems, source: POLICY_FILE };
|
|
143
|
+
}
|
|
144
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
145
|
+
problems.push(`${POLICY_FILE} must be a JSON object; defaults are in effect`);
|
|
146
|
+
return { policy, problems, source: POLICY_FILE };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (raw.mode !== undefined) {
|
|
150
|
+
if (raw.mode === 'warn' || raw.mode === 'block') policy.mode = raw.mode;
|
|
151
|
+
else problems.push(`${POLICY_FILE}: "mode" must be "warn" or "block", got ${JSON.stringify(raw.mode)}; "warn" is in effect`);
|
|
152
|
+
}
|
|
153
|
+
for (const key of ['testDirs', 'testPatterns', 'sourceExtensions', 'nonCodeExtensions', 'ignore']) {
|
|
154
|
+
if (raw[key] === undefined) continue;
|
|
155
|
+
if (!Array.isArray(raw[key]) || raw[key].some((v) => typeof v !== 'string' || !v)) {
|
|
156
|
+
problems.push(`${POLICY_FILE}: "${key}" must be an array of non-empty strings; it is ignored`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const add = key === 'sourceExtensions' || key === 'nonCodeExtensions'
|
|
160
|
+
? raw[key].map((v) => v.replace(/^\./, '').toLowerCase())
|
|
161
|
+
: raw[key];
|
|
162
|
+
policy[key] = [...policy[key], ...add];
|
|
163
|
+
}
|
|
164
|
+
if (raw.exempt !== undefined) {
|
|
165
|
+
if (!Array.isArray(raw.exempt)) {
|
|
166
|
+
problems.push(`${POLICY_FILE}: "exempt" must be an array; it is ignored`);
|
|
167
|
+
} else {
|
|
168
|
+
raw.exempt.forEach((e, i) => {
|
|
169
|
+
const okPattern = e && typeof e.pattern === 'string' && e.pattern.trim();
|
|
170
|
+
const okReason = e && typeof e.reason === 'string' && e.reason.trim();
|
|
171
|
+
if (!okPattern) problems.push(`${POLICY_FILE}: exempt[${i}] has no "pattern"; it is ignored`);
|
|
172
|
+
else if (!okReason) problems.push(`${POLICY_FILE}: exempt[${i}] ("${e.pattern}") has no "reason" and is NOT honored — an exemption must say why`);
|
|
173
|
+
else policy.exempt.push({ pattern: e.pattern, reason: e.reason.trim() });
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { policy, problems, source: POLICY_FILE };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Classify one project-relative path.
|
|
182
|
+
* @returns {{ kind: 'ignored'|'noncode'|'test'|'source'|'unclassified', ext: string, exempt?: {pattern:string, reason:string} }}
|
|
183
|
+
*/
|
|
184
|
+
export function classifyPath(relPath, policy = DEFAULT_POLICY) {
|
|
185
|
+
const p = norm(relPath);
|
|
186
|
+
const segments = p.split('/');
|
|
187
|
+
const base = segments[segments.length - 1];
|
|
188
|
+
const dirs = segments.slice(0, -1);
|
|
189
|
+
const ext = extOf(p);
|
|
190
|
+
const sets = {
|
|
191
|
+
source: new Set(policy.sourceExtensions || DEFAULT_POLICY.sourceExtensions),
|
|
192
|
+
noncode: new Set(policy.nonCodeExtensions || DEFAULT_POLICY.nonCodeExtensions),
|
|
193
|
+
names: new Set(policy.nonCodeNames || DEFAULT_POLICY.nonCodeNames),
|
|
194
|
+
ignoreDirs: new Set(policy.ignoreDirs || DEFAULT_POLICY.ignoreDirs)
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
if (dirs.some((d) => sets.ignoreDirs.has(d))) return { kind: 'ignored', ext };
|
|
198
|
+
if ((policy.ignore || DEFAULT_POLICY.ignore).some((g) => matchesGlob(p, g))) return { kind: 'ignored', ext };
|
|
199
|
+
|
|
200
|
+
const docsLike = sets.noncode.has(ext) || sets.names.has(base) || sets.names.has(base.replace(/\.[^.]*$/, ''));
|
|
201
|
+
const inTestDir = dirs.some((d) => (policy.testDirs || DEFAULT_POLICY.testDirs).includes(d));
|
|
202
|
+
const testNamed = (policy.testPatterns || DEFAULT_POLICY.testPatterns).some((g) => matchesGlob(p, g));
|
|
203
|
+
|
|
204
|
+
// A test is a test even when its data is JSON; but a README inside tests/ is documentation.
|
|
205
|
+
if ((inTestDir || testNamed) && !['md', 'mdx', 'txt', 'rst', 'adoc'].includes(ext)) return { kind: 'test', ext };
|
|
206
|
+
// Dot-files (.prettierrc, .eslintrc.js, .env.local ...) are tool configuration, not product code.
|
|
207
|
+
if (docsLike || base.startsWith('.')) return { kind: 'noncode', ext };
|
|
208
|
+
if (sets.source.has(ext)) {
|
|
209
|
+
const exempt = (policy.exempt || []).find((e) => matchesGlob(p, e.pattern));
|
|
210
|
+
return exempt ? { kind: 'source', ext, exempt } : { kind: 'source', ext };
|
|
211
|
+
}
|
|
212
|
+
return { kind: 'unclassified', ext };
|
|
213
|
+
}
|
|
214
|
+
// ---- END SHARED POLICY ----
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-line summaries `uds update` prints, kept apart from the command so what they say can be tested
|
|
3
|
+
* without running an update.
|
|
4
|
+
*
|
|
5
|
+
* @module utils/update-summary
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const agentOf = (inst) => (typeof inst === 'string' ? inst : inst.agent);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* "Updated N commands for M AI tools: <where>" (XSPEC-454 R4).
|
|
12
|
+
*
|
|
13
|
+
* The number of tools and the number of commands are different numbers and are filled into different
|
|
14
|
+
* slots. The summary used to reuse `commandsUpdated`, a message worded "N AI tools" in every language,
|
|
15
|
+
* and feed it the command-file count: one OpenCode install printed "Updated Commands for 51 AI tools".
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} msg - `t().commands.update`
|
|
18
|
+
* @param {Array<string|{agent: string}>} installations - The targets the commands went to
|
|
19
|
+
* @param {{ totalInstalled: number }} result - The installer's result
|
|
20
|
+
* @param {string} locations - Already-formatted list of where they went
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function commandsUpdatedMessage(msg, installations, result, locations) {
|
|
24
|
+
const tools = new Set((installations || []).map(agentOf)).size;
|
|
25
|
+
return (msg.commandsUpdatedCounts || 'Updated {commands} commands for {tools} AI tool(s): {locations}')
|
|
26
|
+
.replace('{tools}', tools)
|
|
27
|
+
.replace('{commands}', result.totalInstalled)
|
|
28
|
+
.replace('{locations}', locations);
|
|
29
|
+
}
|
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.14.0-beta.
|
|
3
|
+
"version": "6.14.0-beta.5",
|
|
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.14.0-beta.
|
|
61
|
+
"version": "6.14.0-beta.5"
|
|
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.14.0-beta.
|
|
68
|
+
"version": "6.14.0-beta.5",
|
|
69
69
|
"note": "Skills are now included in the main repository under skills/"
|
|
70
70
|
}
|
|
71
71
|
},
|
|
@@ -2309,7 +2309,7 @@
|
|
|
2309
2309
|
"id": "license-compliance",
|
|
2310
2310
|
"name": "License Compliance Standards",
|
|
2311
2311
|
"nameZh": "授權合規標準",
|
|
2312
|
-
"version": "6.14.0-beta.
|
|
2312
|
+
"version": "6.14.0-beta.5",
|
|
2313
2313
|
"source": {
|
|
2314
2314
|
"human": "core/license-compliance.md",
|
|
2315
2315
|
"ai": "ai/standards/license-compliance.ai.yaml"
|
|
@@ -2321,7 +2321,7 @@
|
|
|
2321
2321
|
"id": "verification-oracle",
|
|
2322
2322
|
"name": "Verification Oracle Standards",
|
|
2323
2323
|
"nameZh": "驗證 Oracle 標準",
|
|
2324
|
-
"version": "6.14.0-beta.
|
|
2324
|
+
"version": "6.14.0-beta.5",
|
|
2325
2325
|
"source": {
|
|
2326
2326
|
"human": "core/verification-oracle.md",
|
|
2327
2327
|
"ai": "ai/standards/verification-oracle.ai.yaml"
|
|
@@ -2333,7 +2333,7 @@
|
|
|
2333
2333
|
"id": "model-provenance",
|
|
2334
2334
|
"name": "Model Provenance Policy Standards",
|
|
2335
2335
|
"nameZh": "模型來源政策標準",
|
|
2336
|
-
"version": "6.14.0-beta.
|
|
2336
|
+
"version": "6.14.0-beta.5",
|
|
2337
2337
|
"source": {
|
|
2338
2338
|
"human": "core/model-provenance.md",
|
|
2339
2339
|
"ai": "ai/standards/model-provenance.ai.yaml"
|
|
@@ -2345,7 +2345,7 @@
|
|
|
2345
2345
|
"id": "resource-cost-boundary",
|
|
2346
2346
|
"name": "Resource / Cost Boundary Declaration Standards",
|
|
2347
2347
|
"nameZh": "資源/成本邊界宣告標準",
|
|
2348
|
-
"version": "6.14.0-beta.
|
|
2348
|
+
"version": "6.14.0-beta.5",
|
|
2349
2349
|
"source": {
|
|
2350
2350
|
"human": "core/resource-cost-boundary.md",
|
|
2351
2351
|
"ai": "ai/standards/resource-cost-boundary.ai.yaml"
|