universal-dev-standards 6.9.0 → 6.11.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 +2 -0
- package/bundled/core/agent-communication-protocol.md +8 -0
- package/bundled/core/branch-completion.md +8 -0
- package/bundled/core/change-batching-standards.md +8 -0
- package/bundled/core/execution-history.md +8 -0
- package/bundled/core/pipeline-integration-standards.md +8 -0
- package/bundled/core/workflow-enforcement.md +8 -0
- package/bundled/core/workflow-state-protocol.md +8 -0
- package/bundled/locales/zh-CN/CHANGELOG.md +49 -3
- package/bundled/locales/zh-CN/README.md +1 -1
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/core/agent-communication-protocol.md +7 -0
- package/bundled/locales/zh-CN/core/branch-completion.md +7 -0
- package/bundled/locales/zh-CN/core/change-batching-standards.md +7 -0
- package/bundled/locales/zh-CN/core/execution-history.md +7 -0
- package/bundled/locales/zh-CN/core/pipeline-integration-standards.md +7 -0
- package/bundled/locales/zh-CN/core/workflow-enforcement.md +7 -0
- package/bundled/locales/zh-CN/core/workflow-state-protocol.md +7 -0
- package/bundled/locales/zh-CN/docs/CHEATSHEET.md +1 -1
- package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +52 -5
- package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +3 -1
- package/bundled/locales/zh-CN/docs/MIGRATION-v6.md +8 -4
- package/bundled/locales/zh-TW/CHANGELOG.md +50 -3
- package/bundled/locales/zh-TW/README.md +1 -1
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/agent-communication-protocol.md +7 -0
- package/bundled/locales/zh-TW/core/branch-completion.md +7 -0
- package/bundled/locales/zh-TW/core/change-batching-standards.md +7 -0
- package/bundled/locales/zh-TW/core/execution-history.md +7 -0
- package/bundled/locales/zh-TW/core/pipeline-integration-standards.md +7 -0
- package/bundled/locales/zh-TW/core/workflow-enforcement.md +7 -0
- package/bundled/locales/zh-TW/core/workflow-state-protocol.md +7 -0
- package/bundled/locales/zh-TW/docs/CHEATSHEET.md +1 -1
- package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +52 -5
- package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +3 -1
- package/bundled/locales/zh-TW/docs/MIGRATION-v6.md +8 -4
- package/bundled/locales/zh-TW/integrations/claude-code/README.md +14 -5
- package/package.json +7 -6
- package/src/commands/check.js +413 -44
- package/src/commands/config.js +34 -28
- package/src/commands/init.js +37 -7
- package/src/commands/spec.js +2 -2
- package/src/commands/update.js +560 -74
- package/src/core/manifest.js +39 -1
- package/src/flows/init-flow.js +9 -1
- package/src/generators/layered-claudemd.js +13 -4
- package/src/i18n/messages.js +39 -3
- package/src/installers/integration-installer.js +17 -10
- package/src/installers/manifest-installer.js +4 -0
- package/src/installers/skills-installer.js +4 -4
- package/src/installers/standards-installer.js +3 -3
- package/src/prompts/init.js +33 -4
- package/src/reconciler/actual-state-scanner.js +29 -2
- package/src/reconciler/desired-state-calculator.js +51 -2
- package/src/reconciler/diff-engine.js +76 -5
- package/src/reconciler/plan-executor.js +48 -27
- package/src/utils/hasher.js +61 -5
- package/src/utils/integration-generator.js +431 -92
- package/src/utils/marker-locator.js +140 -0
- package/src/utils/reference-sync.js +156 -8
- package/src/utils/registry.js +57 -0
- package/src/utils/spinner.js +31 -0
- package/standards-registry.json +8 -8
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared marker-block location logic for UDS-managed sections in integration
|
|
3
|
+
* files (CLAUDE.md, AGENTS.md, etc.).
|
|
4
|
+
*
|
|
5
|
+
* XSPEC adopter-report Q5 (v3.5.0–6.10.0): every call site that located the
|
|
6
|
+
* `<!-- UDS:STANDARDS:START -->` / `<!-- UDS:STANDARDS:END -->` pair used
|
|
7
|
+
* `content.indexOf(markers.start)` / `.indexOf(markers.end)` directly. That
|
|
8
|
+
* matches the marker text anywhere in the file — including inside a sentence
|
|
9
|
+
* that merely *mentions* it (this project's own CLAUDE.md explains the marker
|
|
10
|
+
* syntax in prose, `<!-- UDS:STANDARDS:START -->`, inline) or inside a fenced
|
|
11
|
+
* code sample quoting it. A write path built on that wrong index then treated
|
|
12
|
+
* everything between the mention and the real END marker as "the UDS block"
|
|
13
|
+
* and deleted it — including the user's own content in between.
|
|
14
|
+
*
|
|
15
|
+
* The fix: a marker only counts when, after trimming, it occupies an entire
|
|
16
|
+
* line by itself, and that line is not inside a fenced code block (``` or
|
|
17
|
+
* ~~~). Every reader of a marker pair in this codebase must go through
|
|
18
|
+
* `locateMarkerBlock`/`tryLocateMarkerBlock` instead of indexOf/includes —
|
|
19
|
+
* enforced by a static-scan guard test.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} MarkerLine
|
|
24
|
+
* @property {number} offset - Character offset of the marker text itself
|
|
25
|
+
* within `content` (not the start of the line — leading whitespace before
|
|
26
|
+
* the marker, if any, is excluded).
|
|
27
|
+
* @property {number} line - 1-based line number.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Find every standalone-line, non-fenced-code occurrence of `markerText`.
|
|
32
|
+
* @param {string} content
|
|
33
|
+
* @param {string} markerText
|
|
34
|
+
* @returns {MarkerLine[]}
|
|
35
|
+
*/
|
|
36
|
+
function findMarkerLines(content, markerText) {
|
|
37
|
+
const lines = content.split('\n');
|
|
38
|
+
const occurrences = [];
|
|
39
|
+
let offset = 0;
|
|
40
|
+
let inFence = false;
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < lines.length; i++) {
|
|
43
|
+
const rawLine = lines[i];
|
|
44
|
+
const trimmed = rawLine.trim();
|
|
45
|
+
|
|
46
|
+
if (/^(`{3,}|~{3,})/.test(trimmed)) {
|
|
47
|
+
// Fenced code block delimiter (``` or ```lang, closing ``` too) — a
|
|
48
|
+
// marker string appearing between a pair of these is a documentation
|
|
49
|
+
// sample, not a real boundary. This intentionally does not try to
|
|
50
|
+
// distinguish open/close by matching fence characters or length; any
|
|
51
|
+
// triple-backtick-or-tilde line toggles the state, matching how
|
|
52
|
+
// Markdown renderers treat fences in practice.
|
|
53
|
+
inFence = !inFence;
|
|
54
|
+
} else if (!inFence && trimmed === markerText) {
|
|
55
|
+
const leading = rawLine.length - rawLine.trimStart().length;
|
|
56
|
+
occurrences.push({ offset: offset + leading, line: i + 1 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
offset += rawLine.length + 1; // +1 for the '\n' that split('\n') consumed
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return occurrences;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Thrown when a marker's start or end text has more than one standalone,
|
|
67
|
+
* non-fenced occurrence in a file — the file is ambiguous and no caller
|
|
68
|
+
* should guess which one is the real boundary.
|
|
69
|
+
*/
|
|
70
|
+
export class AmbiguousMarkerError extends Error {
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} markerText
|
|
73
|
+
* @param {number[]} lines - 1-based line numbers of every occurrence found.
|
|
74
|
+
*/
|
|
75
|
+
constructor(markerText, lines) {
|
|
76
|
+
super(
|
|
77
|
+
`Found ${lines.length} standalone occurrences of marker "${markerText}" ` +
|
|
78
|
+
`on line(s) ${lines.join(', ')} — expected exactly one. Refusing to guess ` +
|
|
79
|
+
'which one bounds the UDS-managed block.'
|
|
80
|
+
);
|
|
81
|
+
this.name = 'AmbiguousMarkerError';
|
|
82
|
+
this.markerText = markerText;
|
|
83
|
+
this.lines = lines;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Locate the single UDS marker block in `content`.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} content
|
|
91
|
+
* @param {{start: string, end: string}} markers
|
|
92
|
+
* @returns {{startIdx: number, endIdx: number, startLine: number, endLine: number} | null}
|
|
93
|
+
* `null` means "this file has no UDS block" — the same meaning the old
|
|
94
|
+
* `startIdx === -1` check carried. This is not an error.
|
|
95
|
+
* @throws {AmbiguousMarkerError} when more than one valid START or END line
|
|
96
|
+
* exists — the file has two or more marker pairs, or a corrupted one.
|
|
97
|
+
*/
|
|
98
|
+
export function locateMarkerBlock(content, markers) {
|
|
99
|
+
const starts = findMarkerLines(content, markers.start);
|
|
100
|
+
const ends = findMarkerLines(content, markers.end);
|
|
101
|
+
|
|
102
|
+
if (starts.length > 1) {
|
|
103
|
+
throw new AmbiguousMarkerError(markers.start, starts.map((s) => s.line));
|
|
104
|
+
}
|
|
105
|
+
if (ends.length > 1) {
|
|
106
|
+
throw new AmbiguousMarkerError(markers.end, ends.map((e) => e.line));
|
|
107
|
+
}
|
|
108
|
+
if (starts.length === 0 || ends.length === 0) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const startIdx = starts[0].offset;
|
|
113
|
+
const endIdx = ends[0].offset;
|
|
114
|
+
if (endIdx <= startIdx) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { startIdx, endIdx, startLine: starts[0].line, endLine: ends[0].line };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Non-throwing variant of `locateMarkerBlock` for callers that must not
|
|
123
|
+
* crash a broader scan on one ambiguous file (e.g. a full-project state
|
|
124
|
+
* scan reconciling many files at once). Ambiguity is reported through the
|
|
125
|
+
* return value instead of an exception.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} content
|
|
128
|
+
* @param {{start: string, end: string}} markers
|
|
129
|
+
* @returns {{ok: true, block: ReturnType<typeof locateMarkerBlock>} | {ok: false, error: AmbiguousMarkerError}}
|
|
130
|
+
*/
|
|
131
|
+
export function tryLocateMarkerBlock(content, markers) {
|
|
132
|
+
try {
|
|
133
|
+
return { ok: true, block: locateMarkerBlock(content, markers) };
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error instanceof AmbiguousMarkerError) {
|
|
136
|
+
return { ok: false, error };
|
|
137
|
+
}
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Reference Sync Utilities
|
|
3
6
|
*
|
|
@@ -109,6 +112,56 @@ export function parseReferences(content) {
|
|
|
109
112
|
return Array.from(references);
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Every `.standards/…` or `core/…` path mentioned anywhere in the file that is
|
|
117
|
+
* not on disk.
|
|
118
|
+
*
|
|
119
|
+
* `parseReferences` only reads `Reference:`/`參考:` lines, which is the right
|
|
120
|
+
* scope for reference *sync* — those lines are the ones UDS generates and owns.
|
|
121
|
+
* It is the wrong scope for "does this instruction point at anything": an older
|
|
122
|
+
* `uds init` left prose outside the marker block saying "優先讀取 core/ 中的精簡
|
|
123
|
+
* 規則(例如 core/testing-standards.md)", and on a `format: ai` project no
|
|
124
|
+
* `core/` directory is ever installed. An AI tool following that finds nothing,
|
|
125
|
+
* and nothing in the toolchain looked. Reported 2026-09-16.
|
|
126
|
+
*
|
|
127
|
+
* Existence-based, not shape-based, and deliberately so: a project that has its
|
|
128
|
+
* own `core/` directory is not doing anything wrong, and a rule about the shape
|
|
129
|
+
* of the path could not tell the two apart.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} content - File content
|
|
132
|
+
* @param {string|null} projectPath - Project root; without it there is nothing
|
|
133
|
+
* to check against and the answer is "nothing", not a guess
|
|
134
|
+
* @returns {string[]} Distinct paths that do not resolve, in first-seen order
|
|
135
|
+
*/
|
|
136
|
+
export function findBrokenPathMentions(content, projectPath) {
|
|
137
|
+
if (!projectPath || typeof content !== 'string') return [];
|
|
138
|
+
|
|
139
|
+
// Not preceded by `/` or a word character: that is what keeps
|
|
140
|
+
// `https://…/core/testing-standards.md` out of the results.
|
|
141
|
+
//
|
|
142
|
+
// The terminator class carries the full-width punctuation as well as the
|
|
143
|
+
// ASCII. These instructions are written in Chinese, and `(例如
|
|
144
|
+
// core/testing-standards.md)` ends in `)` — without it the path captured is
|
|
145
|
+
// `core/testing-standards.md)`, which exists nowhere and matches nothing, so
|
|
146
|
+
// the finding would be real and the path in it wrong.
|
|
147
|
+
const pattern = /(^|[^\w/\\])((?:\.standards|core)\/[^\s\n)\]`,;'")】」』〉》,。、;:!?]+)/g;
|
|
148
|
+
const seen = new Set();
|
|
149
|
+
const broken = [];
|
|
150
|
+
|
|
151
|
+
for (const match of content.matchAll(pattern)) {
|
|
152
|
+
const cleaned = match[2].replace(/[.,;:!?、,。]+$/, '');
|
|
153
|
+
if (!cleaned || seen.has(cleaned)) continue;
|
|
154
|
+
seen.add(cleaned);
|
|
155
|
+
try {
|
|
156
|
+
if (!existsSync(join(projectPath, cleaned))) broken.push(cleaned);
|
|
157
|
+
} catch {
|
|
158
|
+
// An unreadable path is not evidence of a broken reference.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return broken;
|
|
163
|
+
}
|
|
164
|
+
|
|
112
165
|
/**
|
|
113
166
|
* Get standard source paths for a category
|
|
114
167
|
*
|
|
@@ -176,7 +229,27 @@ function categoryForStandard(pathOrName) {
|
|
|
176
229
|
* - missingRefs: Standards in manifest but not referenced in integration file
|
|
177
230
|
* - syncedRefs: Standards that are properly synced
|
|
178
231
|
*/
|
|
179
|
-
export function compareStandardsWithReferences(manifestStandards, integrationReferences) {
|
|
232
|
+
export function compareStandardsWithReferences(manifestStandards, integrationReferences, context = {}) {
|
|
233
|
+
const { projectPath = null, options = {} } = context;
|
|
234
|
+
|
|
235
|
+
// 🔴 Existence is a separate question from adoption, and only one of the two
|
|
236
|
+
// was ever asked. `.standards/anti-hallucination.md` passed as "synced"
|
|
237
|
+
// because the manifest adopts `anti-hallucination` — while the project holds
|
|
238
|
+
// `anti-hallucination.ai.yaml` and the `.md` file is not on disk at all
|
|
239
|
+
// (reproduced on 6.9.0, 2026-09-16). An adopter's AI tool follows that link
|
|
240
|
+
// and finds nothing. Without a projectPath there is no disk to ask, so the
|
|
241
|
+
// list stays empty rather than guessing.
|
|
242
|
+
const danglingRefs = projectPath
|
|
243
|
+
? integrationReferences.filter(ref => !existsSync(join(projectPath, '.standards', ref)))
|
|
244
|
+
: [];
|
|
245
|
+
|
|
246
|
+
// Options are adopted in `manifest.options`, not `manifest.standards`, so
|
|
247
|
+
// every `.standards/options/...` reference was reported as "not in manifest".
|
|
248
|
+
const optionStems = new Set(
|
|
249
|
+
Object.values(options || {})
|
|
250
|
+
.filter(v => typeof v === 'string')
|
|
251
|
+
.map(v => standardStem(v))
|
|
252
|
+
);
|
|
180
253
|
// The manifest is the source of truth for what this project adopted, so ask
|
|
181
254
|
// it directly. The category map below is a hand-written table covering a
|
|
182
255
|
// small fraction of the standards UDS ships (compare its size against
|
|
@@ -207,6 +280,10 @@ export function compareStandardsWithReferences(manifestStandards, integrationRef
|
|
|
207
280
|
// stem (extension- and directory-insensitive) or by legacy category name.
|
|
208
281
|
const orphanedRefs = integrationReferences.filter(ref => {
|
|
209
282
|
if (manifestStems.has(standardStem(ref))) return false;
|
|
283
|
+
if (optionStems.has(standardStem(ref))) return false;
|
|
284
|
+
// An option file that is on disk is adopted by definition — it got there
|
|
285
|
+
// because this project selected it.
|
|
286
|
+
if (ref.startsWith('options/') && projectPath && existsSync(join(projectPath, '.standards', ref))) return false;
|
|
210
287
|
const category = categoryForStandard(ref);
|
|
211
288
|
return !category || !manifestCategories.has(category);
|
|
212
289
|
});
|
|
@@ -221,15 +298,34 @@ export function compareStandardsWithReferences(manifestStandards, integrationRef
|
|
|
221
298
|
}
|
|
222
299
|
const missingCategories = [...manifestCategories].filter(cat => !refCategories.has(cat));
|
|
223
300
|
// Convert missing categories back to representative filenames for reporting
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
301
|
+
// 🔴 `CATEGORY_TO_STANDARDS` is a hand-written table of pre-6.0.0 filenames,
|
|
302
|
+
// so this list named files UDS has not shipped for two majors
|
|
303
|
+
// (`git-workflow.md`, `error-code-standards.md`, `project-structure.md`).
|
|
304
|
+
// Report only what this project actually holds. With no projectPath there is
|
|
305
|
+
// no disk to ask, so the legacy names stand — callers that pass one get the
|
|
306
|
+
// filtered list.
|
|
307
|
+
const missingRefs = !projectPath
|
|
308
|
+
? missingCategories.flatMap(cat => (CATEGORY_TO_STANDARDS[cat] || []).map(p => p.split('/').pop()))
|
|
309
|
+
: missingCategories.flatMap(cat => {
|
|
310
|
+
const paths = CATEGORY_TO_STANDARDS[cat] || [];
|
|
311
|
+
return paths
|
|
312
|
+
.map(p => p.split('/').pop())
|
|
313
|
+
.map(name => {
|
|
314
|
+
const stem = standardStem(name);
|
|
315
|
+
for (const candidate of [`${stem}.ai.yaml`, `${stem}.md`]) {
|
|
316
|
+
if (existsSync(join(projectPath, '.standards', candidate))) return candidate;
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
})
|
|
320
|
+
.filter(Boolean);
|
|
321
|
+
});
|
|
228
322
|
|
|
229
323
|
// Properly synced references
|
|
230
|
-
const syncedRefs = integrationReferences.filter(
|
|
324
|
+
const syncedRefs = integrationReferences.filter(
|
|
325
|
+
ref => !orphanedRefs.includes(ref) && !danglingRefs.includes(ref)
|
|
326
|
+
);
|
|
231
327
|
|
|
232
|
-
return { orphanedRefs, missingRefs, syncedRefs };
|
|
328
|
+
return { orphanedRefs, missingRefs, syncedRefs, danglingRefs };
|
|
233
329
|
}
|
|
234
330
|
|
|
235
331
|
/**
|
|
@@ -242,7 +338,12 @@ export function calculateCategoriesFromStandards(standards) {
|
|
|
242
338
|
const categories = new Set();
|
|
243
339
|
|
|
244
340
|
for (const std of standards) {
|
|
245
|
-
|
|
341
|
+
// XSPEC adopter-report Q1: getStandardCategory is keyed by filename and
|
|
342
|
+
// returns null for a bare manifest stem ('commit-message'), which is
|
|
343
|
+
// exactly what a 3.4.0 manifest stores — this emptied the whole category
|
|
344
|
+
// set on a real install. categoryForStandard (below) tries the stem
|
|
345
|
+
// against both known extensions first.
|
|
346
|
+
const category = categoryForStandard(std);
|
|
246
347
|
if (category) {
|
|
247
348
|
categories.add(category);
|
|
248
349
|
}
|
|
@@ -251,6 +352,53 @@ export function calculateCategoriesFromStandards(standards) {
|
|
|
251
352
|
return Array.from(categories);
|
|
252
353
|
}
|
|
253
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Whether a stored `categories` array is broken and should not be trusted:
|
|
357
|
+
* missing/not-an-array, empty, or containing a value this build does not
|
|
358
|
+
* recognize as a category id.
|
|
359
|
+
*
|
|
360
|
+
* @param {*} categories - `manifest.integrationConfigs[file].categories`
|
|
361
|
+
* @returns {boolean}
|
|
362
|
+
*/
|
|
363
|
+
function categoriesLookBroken(categories) {
|
|
364
|
+
if (!Array.isArray(categories) || categories.length === 0) return true;
|
|
365
|
+
const known = new Set(Object.keys(CATEGORY_TO_STANDARDS));
|
|
366
|
+
return categories.some((c) => !known.has(c));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Repair `manifest.integrationConfigs[file].categories` wherever it is empty
|
|
371
|
+
* or contains an unrecognized value, recomputing it from `manifest.standards`
|
|
372
|
+
* the same way `calculateCategoriesFromStandards` (used by `--sync-refs`)
|
|
373
|
+
* does.
|
|
374
|
+
*
|
|
375
|
+
* XSPEC adopter-report Q1 follow-up (main-session review): the original Q1
|
|
376
|
+
* fix repaired this only inside `--sync-refs`. A manifest that picked up a
|
|
377
|
+
* broken `categories: []` from an older buggy `--sync-refs` run stayed
|
|
378
|
+
* broken forever afterward — a plain `uds update` reaches its OWN,
|
|
379
|
+
* independent integration-sync code path (update.js's general update flow)
|
|
380
|
+
* which writes `integrationBlockHashes` but never even reads
|
|
381
|
+
* `integrationConfigs`, so nothing there ever corrected it. The next `uds
|
|
382
|
+
* check --restore-missing` then rebuilt the file from that broken stored
|
|
383
|
+
* config and silently dropped sections. Deliberately conservative: an
|
|
384
|
+
* already-valid (if outdated) categories list is left alone — that is
|
|
385
|
+
* `--sync-refs`'s job, not every plain `update`'s — this only self-heals
|
|
386
|
+
* outright corruption (empty or unrecognized values).
|
|
387
|
+
*
|
|
388
|
+
* @param {Object} manifest - Manifest object (mutated in place)
|
|
389
|
+
* @returns {string[]} Paths whose `categories` were repaired
|
|
390
|
+
*/
|
|
391
|
+
export function repairIntegrationConfigCategories(manifest) {
|
|
392
|
+
if (!manifest?.integrationConfigs) return [];
|
|
393
|
+
const repaired = [];
|
|
394
|
+
for (const [file, config] of Object.entries(manifest.integrationConfigs)) {
|
|
395
|
+
if (!config || !categoriesLookBroken(config.categories)) continue;
|
|
396
|
+
config.categories = calculateCategoriesFromStandards(manifest.standards || []);
|
|
397
|
+
repaired.push(file);
|
|
398
|
+
}
|
|
399
|
+
return repaired;
|
|
400
|
+
}
|
|
401
|
+
|
|
254
402
|
/**
|
|
255
403
|
* Get all standard filenames that should be referenced for given categories
|
|
256
404
|
*
|
package/src/utils/registry.js
CHANGED
|
@@ -320,3 +320,60 @@ export function resolveStandardFilename(entry, format = 'ai') {
|
|
|
320
320
|
const parts = source.split(/[/\\]/);
|
|
321
321
|
return parts[parts.length - 1] || null;
|
|
322
322
|
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Every filename UDS can install under `.standards/`, in either format.
|
|
326
|
+
*
|
|
327
|
+
* Built by walking the registry: each standard's own source plus every option
|
|
328
|
+
* choice's source, `ai` and `human` alike. Spanning both formats is deliberate
|
|
329
|
+
* and is why this takes no format argument — a project can hold a file in a
|
|
330
|
+
* format it did not install (an upgrade that changed `format`, a hand-copied
|
|
331
|
+
* file), and calling that "no longer shipped" would be wrong in the opposite
|
|
332
|
+
* direction. The question here is only ever "is this name still ours".
|
|
333
|
+
*
|
|
334
|
+
* This exists so that question has one answer, derived from the registry rather
|
|
335
|
+
* than from the manifest. A manifest that was never cleaned and one that was
|
|
336
|
+
* must not disagree about it.
|
|
337
|
+
*
|
|
338
|
+
* @returns {Set<string>} Filenames (basenames, no directory)
|
|
339
|
+
*/
|
|
340
|
+
let shippableCache = null;
|
|
341
|
+
export function getShippableFilenames() {
|
|
342
|
+
if (shippableCache) return shippableCache;
|
|
343
|
+
|
|
344
|
+
const out = new Set();
|
|
345
|
+
const addPath = (value) => {
|
|
346
|
+
if (typeof value !== 'string' || value.length === 0) return;
|
|
347
|
+
const parts = value.split(/[/\\]/);
|
|
348
|
+
const name = parts[parts.length - 1];
|
|
349
|
+
if (name) out.add(name);
|
|
350
|
+
};
|
|
351
|
+
const addSource = (source) => {
|
|
352
|
+
if (!source) return;
|
|
353
|
+
if (typeof source === 'string') {
|
|
354
|
+
addPath(source);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
for (const value of Object.values(source)) addPath(value);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
for (const std of getAllStandards()) {
|
|
361
|
+
addSource(std.source);
|
|
362
|
+
for (const category of Object.values(std.options || {})) {
|
|
363
|
+
for (const choice of category.choices || []) addSource(choice.source);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
shippableCache = out;
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Does UDS still ship a file by this name, in either format?
|
|
373
|
+
* @param {string} filename - Basename, e.g. `anti-hallucination.ai.yaml`
|
|
374
|
+
* @returns {boolean}
|
|
375
|
+
*/
|
|
376
|
+
export function isShippedFilename(filename) {
|
|
377
|
+
if (typeof filename !== 'string' || filename.length === 0) return false;
|
|
378
|
+
return getShippableFilenames().has(filename);
|
|
379
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A spinner that reports on stdout.
|
|
5
|
+
*
|
|
6
|
+
* ora defaults to stderr. For a spinner that redraws over itself while a pipe
|
|
7
|
+
* consumes stdout, that default is right. It is wrong for this CLI: these lines
|
|
8
|
+
* are progress reporting for a human, they sit between lines the CLI already
|
|
9
|
+
* prints on stdout, and in a non-TTY ora emits each one exactly once, so there
|
|
10
|
+
* is nothing to redraw and nothing to isolate.
|
|
11
|
+
*
|
|
12
|
+
* On Windows PowerShell 5.1 the default is worse than untidy. Any stderr output
|
|
13
|
+
* from a native command is wrapped in a `NativeCommandError`, so a successful
|
|
14
|
+
* `uds update` surfaced as an error — including the line that said it had
|
|
15
|
+
* succeeded. Measured on a clean run before this change: 9 lines on stdout, 2 on
|
|
16
|
+
* stderr, both of them the spinner, one of them `✔ 已重新產生 2 個整合檔案`.
|
|
17
|
+
*
|
|
18
|
+
* A caller that genuinely wants stderr can still ask for it; the default is the
|
|
19
|
+
* only thing that changes.
|
|
20
|
+
*
|
|
21
|
+
* @param {string|Object} options - Spinner text, or an ora options object
|
|
22
|
+
* @returns {import('ora').Ora}
|
|
23
|
+
*/
|
|
24
|
+
export function createSpinner(options) {
|
|
25
|
+
if (typeof options === 'string') {
|
|
26
|
+
return ora({ text: options, stream: process.stdout });
|
|
27
|
+
}
|
|
28
|
+
return ora({ stream: process.stdout, ...options });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default createSpinner;
|
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.11.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.11.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.11.0",
|
|
69
69
|
"note": "Skills are now included in the main repository under skills/"
|
|
70
70
|
}
|
|
71
71
|
},
|
|
@@ -1091,7 +1091,7 @@
|
|
|
1091
1091
|
"name": "Full Coverage Testing Standards",
|
|
1092
1092
|
"nameZh": "全覆蓋測試標準",
|
|
1093
1093
|
"source": {
|
|
1094
|
-
"human": "core/testing
|
|
1094
|
+
"human": "core/full-coverage-testing.md",
|
|
1095
1095
|
"ai": "ai/standards/full-coverage-testing.ai.yaml"
|
|
1096
1096
|
},
|
|
1097
1097
|
"category": "skill",
|
|
@@ -2283,7 +2283,7 @@
|
|
|
2283
2283
|
"id": "license-compliance",
|
|
2284
2284
|
"name": "License Compliance Standards",
|
|
2285
2285
|
"nameZh": "授權合規標準",
|
|
2286
|
-
"version": "6.
|
|
2286
|
+
"version": "6.11.0",
|
|
2287
2287
|
"source": {
|
|
2288
2288
|
"human": "core/license-compliance.md",
|
|
2289
2289
|
"ai": "ai/standards/license-compliance.ai.yaml"
|
|
@@ -2295,7 +2295,7 @@
|
|
|
2295
2295
|
"id": "verification-oracle",
|
|
2296
2296
|
"name": "Verification Oracle Standards",
|
|
2297
2297
|
"nameZh": "驗證 Oracle 標準",
|
|
2298
|
-
"version": "6.
|
|
2298
|
+
"version": "6.11.0",
|
|
2299
2299
|
"source": {
|
|
2300
2300
|
"human": "core/verification-oracle.md",
|
|
2301
2301
|
"ai": "ai/standards/verification-oracle.ai.yaml"
|
|
@@ -2307,7 +2307,7 @@
|
|
|
2307
2307
|
"id": "model-provenance",
|
|
2308
2308
|
"name": "Model Provenance Policy Standards",
|
|
2309
2309
|
"nameZh": "模型來源政策標準",
|
|
2310
|
-
"version": "6.
|
|
2310
|
+
"version": "6.11.0",
|
|
2311
2311
|
"source": {
|
|
2312
2312
|
"human": "core/model-provenance.md",
|
|
2313
2313
|
"ai": "ai/standards/model-provenance.ai.yaml"
|
|
@@ -2319,7 +2319,7 @@
|
|
|
2319
2319
|
"id": "resource-cost-boundary",
|
|
2320
2320
|
"name": "Resource / Cost Boundary Declaration Standards",
|
|
2321
2321
|
"nameZh": "資源/成本邊界宣告標準",
|
|
2322
|
-
"version": "6.
|
|
2322
|
+
"version": "6.11.0",
|
|
2323
2323
|
"source": {
|
|
2324
2324
|
"human": "core/resource-cost-boundary.md",
|
|
2325
2325
|
"ai": "ai/standards/resource-cost-boundary.ai.yaml"
|