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.
Files changed (63) hide show
  1. package/bin/uds.js +2 -0
  2. package/bundled/core/agent-communication-protocol.md +8 -0
  3. package/bundled/core/branch-completion.md +8 -0
  4. package/bundled/core/change-batching-standards.md +8 -0
  5. package/bundled/core/execution-history.md +8 -0
  6. package/bundled/core/pipeline-integration-standards.md +8 -0
  7. package/bundled/core/workflow-enforcement.md +8 -0
  8. package/bundled/core/workflow-state-protocol.md +8 -0
  9. package/bundled/locales/zh-CN/CHANGELOG.md +49 -3
  10. package/bundled/locales/zh-CN/README.md +1 -1
  11. package/bundled/locales/zh-CN/SECURITY.md +1 -1
  12. package/bundled/locales/zh-CN/core/agent-communication-protocol.md +7 -0
  13. package/bundled/locales/zh-CN/core/branch-completion.md +7 -0
  14. package/bundled/locales/zh-CN/core/change-batching-standards.md +7 -0
  15. package/bundled/locales/zh-CN/core/execution-history.md +7 -0
  16. package/bundled/locales/zh-CN/core/pipeline-integration-standards.md +7 -0
  17. package/bundled/locales/zh-CN/core/workflow-enforcement.md +7 -0
  18. package/bundled/locales/zh-CN/core/workflow-state-protocol.md +7 -0
  19. package/bundled/locales/zh-CN/docs/CHEATSHEET.md +1 -1
  20. package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +52 -5
  21. package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +3 -1
  22. package/bundled/locales/zh-CN/docs/MIGRATION-v6.md +8 -4
  23. package/bundled/locales/zh-TW/CHANGELOG.md +50 -3
  24. package/bundled/locales/zh-TW/README.md +1 -1
  25. package/bundled/locales/zh-TW/SECURITY.md +1 -1
  26. package/bundled/locales/zh-TW/core/agent-communication-protocol.md +7 -0
  27. package/bundled/locales/zh-TW/core/branch-completion.md +7 -0
  28. package/bundled/locales/zh-TW/core/change-batching-standards.md +7 -0
  29. package/bundled/locales/zh-TW/core/execution-history.md +7 -0
  30. package/bundled/locales/zh-TW/core/pipeline-integration-standards.md +7 -0
  31. package/bundled/locales/zh-TW/core/workflow-enforcement.md +7 -0
  32. package/bundled/locales/zh-TW/core/workflow-state-protocol.md +7 -0
  33. package/bundled/locales/zh-TW/docs/CHEATSHEET.md +1 -1
  34. package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +52 -5
  35. package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +3 -1
  36. package/bundled/locales/zh-TW/docs/MIGRATION-v6.md +8 -4
  37. package/bundled/locales/zh-TW/integrations/claude-code/README.md +14 -5
  38. package/package.json +7 -6
  39. package/src/commands/check.js +413 -44
  40. package/src/commands/config.js +34 -28
  41. package/src/commands/init.js +37 -7
  42. package/src/commands/spec.js +2 -2
  43. package/src/commands/update.js +560 -74
  44. package/src/core/manifest.js +39 -1
  45. package/src/flows/init-flow.js +9 -1
  46. package/src/generators/layered-claudemd.js +13 -4
  47. package/src/i18n/messages.js +39 -3
  48. package/src/installers/integration-installer.js +17 -10
  49. package/src/installers/manifest-installer.js +4 -0
  50. package/src/installers/skills-installer.js +4 -4
  51. package/src/installers/standards-installer.js +3 -3
  52. package/src/prompts/init.js +33 -4
  53. package/src/reconciler/actual-state-scanner.js +29 -2
  54. package/src/reconciler/desired-state-calculator.js +51 -2
  55. package/src/reconciler/diff-engine.js +76 -5
  56. package/src/reconciler/plan-executor.js +48 -27
  57. package/src/utils/hasher.js +61 -5
  58. package/src/utils/integration-generator.js +431 -92
  59. package/src/utils/marker-locator.js +140 -0
  60. package/src/utils/reference-sync.js +156 -8
  61. package/src/utils/registry.js +57 -0
  62. package/src/utils/spinner.js +31 -0
  63. 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
- const missingRefs = missingCategories.flatMap(cat => {
225
- const paths = CATEGORY_TO_STANDARDS[cat] || [];
226
- return paths.map(p => p.split('/').pop());
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(ref => !orphanedRefs.includes(ref));
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
- const category = getStandardCategory(std);
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
  *
@@ -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;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "6.9.0",
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.9.0"
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.9.0",
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-standards.md",
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.9.0",
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.9.0",
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.9.0",
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.9.0",
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"