@llman-sdd/core 0.2.0 → 0.3.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/package.json +1 -1
- package/src/change/lifecycle.ts +8 -0
- package/src/change/resolve.ts +34 -0
- package/src/context/indexStore.ts +41 -0
- package/src/index.ts +4 -0
- package/src/report/show.ts +6 -2
- package/src/report/specs.ts +14 -7
- package/src/review/review.ts +3 -5
- package/src/spec/ir.ts +0 -1
- package/src/spec/parser.ts +4 -7
- package/src/validation/changeCheck.ts +33 -2
- package/src/validation/validate.ts +11 -3
- package/templates/en/units/skills/validation-hints.md +1 -1
- package/templates/en/units/spec/feature-contract.md +1 -1
- package/templates/zh-Hans/units/skills/validation-hints.md +1 -1
- package/templates/zh-Hans/units/spec/feature-contract.md +1 -1
package/package.json
CHANGED
package/src/change/lifecycle.ts
CHANGED
|
@@ -184,6 +184,14 @@ export function finalizeChange(
|
|
|
184
184
|
if (binding === null) {
|
|
185
185
|
throw new LifecycleError(`change \`${id}\` has no branch binding — run start/attach first`);
|
|
186
186
|
}
|
|
187
|
+
// r15 (v1 r94): finalize runs on the bound branch — any other branch must
|
|
188
|
+
// fail before any write (no switch, no merge, no rename).
|
|
189
|
+
const current = currentBranch(git);
|
|
190
|
+
if (current !== binding.branch) {
|
|
191
|
+
throw new LifecycleError(
|
|
192
|
+
`finalize must run on the bound branch \`${binding.branch}\` (current: ${current ?? 'detached HEAD'})`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
187
195
|
const method = opts.method ?? 'squash';
|
|
188
196
|
const target = opts.into ?? binding.baseBranch ?? defaultBranch(git);
|
|
189
197
|
return mergeRenameCommit(git, io, id, binding.branch, target, method, opts.today, opts.noCommit);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Change-id prefix resolution (peripheral-commands r61, v1 cli spec r112):
|
|
3
|
+
* exact match > unique prefix > multiple candidates > no match, all
|
|
4
|
+
* case-sensitive. Candidates are the active change ids discovered via
|
|
5
|
+
* collectChanges (depth-limited, archive-skipping). Pure — IO is injected.
|
|
6
|
+
*/
|
|
7
|
+
import { collectChanges, type ChangeFsIo } from '../report/collect.ts';
|
|
8
|
+
|
|
9
|
+
export interface ResolvedChangeId {
|
|
10
|
+
id: string;
|
|
11
|
+
viaPrefix: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ChangeIdResolveError extends Error {}
|
|
15
|
+
|
|
16
|
+
export function resolveChangeId(
|
|
17
|
+
io: ChangeFsIo,
|
|
18
|
+
root: string,
|
|
19
|
+
input: string,
|
|
20
|
+
opts: { maxScanDepth?: number } = {},
|
|
21
|
+
): ResolvedChangeId {
|
|
22
|
+
const ids = collectChanges(io, root, new Date(0), opts).map((c) => c.name);
|
|
23
|
+
if (ids.includes(input)) return { id: input, viaPrefix: false };
|
|
24
|
+
const matches = ids.filter((id) => id.startsWith(input));
|
|
25
|
+
if (matches.length === 1) return { id: matches[0] as string, viaPrefix: true };
|
|
26
|
+
if (matches.length > 1) {
|
|
27
|
+
throw new ChangeIdResolveError(
|
|
28
|
+
`change '${input}' matches multiple active changes:\n${matches
|
|
29
|
+
.map((m) => ` - ${m}`)
|
|
30
|
+
.join('\n')}\nDid you mean one of these?`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
throw new ChangeIdResolveError(`change not found: ${input}`);
|
|
34
|
+
}
|
|
@@ -124,3 +124,44 @@ export function loadTree(io: IndexIo): SerializedTreeIndex | null {
|
|
|
124
124
|
return null;
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
+
|
|
128
|
+
export interface AutoRefreshResult {
|
|
129
|
+
tree: SerializedTreeIndex | null;
|
|
130
|
+
rebuilt: boolean;
|
|
131
|
+
error: string | null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* r62: retrieval-side lazy refresh — a missing/corrupted/stale index is
|
|
136
|
+
* rebuilt once (zero-LLM) before retrieval; a failed rebuild surfaces
|
|
137
|
+
* `index_rebuild_failed` instead of a missing/stale quality note.
|
|
138
|
+
*/
|
|
139
|
+
export function loadTreeWithAutoRebuild(
|
|
140
|
+
io: IndexIo,
|
|
141
|
+
specsDir: string,
|
|
142
|
+
entries: readonly SpecEntry[],
|
|
143
|
+
opts: RebuildOpts,
|
|
144
|
+
): AutoRefreshResult {
|
|
145
|
+
const initial = loadTree(io);
|
|
146
|
+
if (initial !== null && checkIndexFreshness(io, specsDir).fresh) {
|
|
147
|
+
return { tree: initial, rebuilt: false, error: null };
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
rebuildIndex(io, specsDir, entries, opts);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return {
|
|
153
|
+
tree: null,
|
|
154
|
+
rebuilt: false,
|
|
155
|
+
error: `auto-rebuild failed: ${(error as Error).message} — run \`llman-sdd index rebuild\``,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const tree = loadTree(io);
|
|
159
|
+
if (tree === null) {
|
|
160
|
+
return {
|
|
161
|
+
tree: null,
|
|
162
|
+
rebuilt: true,
|
|
163
|
+
error: 'auto-rebuild produced an unreadable tree.json — run `llman-sdd index rebuild`',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return { tree, rebuilt: true, error: null };
|
|
167
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -112,6 +112,7 @@ export {
|
|
|
112
112
|
} from './git/spawnGit.ts';
|
|
113
113
|
export { DRAFT_PROPOSAL_TEMPLATE, deriveChangeId, isLegalChangeId } from './change/id.ts';
|
|
114
114
|
export { extractUniqueNumber, harvestUniqueNumbers } from './change/nextId.ts';
|
|
115
|
+
export { ChangeIdResolveError, resolveChangeId, type ResolvedChangeId } from './change/resolve.ts';
|
|
115
116
|
export {
|
|
116
117
|
CHANGES_DIR,
|
|
117
118
|
LifecycleError,
|
|
@@ -184,6 +185,7 @@ export {
|
|
|
184
185
|
} from './report/specHelpers.ts';
|
|
185
186
|
export {
|
|
186
187
|
collectSpecs,
|
|
188
|
+
morphologyOfScenarios,
|
|
187
189
|
renderSpecsJson,
|
|
188
190
|
renderSpecsList,
|
|
189
191
|
type SpecMorphology,
|
|
@@ -235,8 +237,10 @@ export {
|
|
|
235
237
|
TREE_JSON_REL,
|
|
236
238
|
checkIndexFreshness,
|
|
237
239
|
loadTree,
|
|
240
|
+
loadTreeWithAutoRebuild,
|
|
238
241
|
parseLock,
|
|
239
242
|
rebuildIndex,
|
|
243
|
+
type AutoRefreshResult,
|
|
240
244
|
type FreshnessResult,
|
|
241
245
|
type IndexIo,
|
|
242
246
|
type RebuildOpts,
|
package/src/report/show.ts
CHANGED
|
@@ -22,7 +22,11 @@ export interface ShowDeps {
|
|
|
22
22
|
specsDir: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export function showChangeJson(
|
|
25
|
+
export function showChangeJson(
|
|
26
|
+
deps: ShowDeps,
|
|
27
|
+
id: string,
|
|
28
|
+
opts: { matchedViaPrefix?: boolean } = {},
|
|
29
|
+
): Record<string, unknown> {
|
|
26
30
|
const { io, git, root } = deps;
|
|
27
31
|
const dir = `${root}/${CHANGES_DIR}/${id}`;
|
|
28
32
|
const proposalPath = `${dir}/proposal.md`;
|
|
@@ -129,6 +133,6 @@ export function showChangeJson(deps: ShowDeps, id: string): Record<string, unkno
|
|
|
129
133
|
deltaCount: 0,
|
|
130
134
|
deltas: [] as string[],
|
|
131
135
|
gateChecks,
|
|
132
|
-
matchedViaPrefix:
|
|
136
|
+
matchedViaPrefix: opts.matchedViaPrefix === true,
|
|
133
137
|
};
|
|
134
138
|
}
|
package/src/report/specs.ts
CHANGED
|
@@ -10,7 +10,6 @@ import { pad } from './collect.ts';
|
|
|
10
10
|
export interface SpecMorphology {
|
|
11
11
|
ruleCount: number;
|
|
12
12
|
ruleEnforcedCount: number;
|
|
13
|
-
ruleManualCount: number;
|
|
14
13
|
rulePendingCount: number;
|
|
15
14
|
acceptanceCount: number;
|
|
16
15
|
orphanAcceptanceCount: number;
|
|
@@ -27,22 +26,30 @@ export interface SpecSummary {
|
|
|
27
26
|
morphology: SpecMorphology;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
/** Morphology counts shared by `list --specs`, `show <spec> --json`, and the
|
|
30
|
+
* CLI text render — the single source of the enforced/pending two-state rule
|
|
31
|
+
* (rules = @human; enforced = @req-linked acceptance coverage; pending = rest). */
|
|
32
|
+
export function morphologyOfScenarios(
|
|
33
|
+
scenarios: readonly { classification: string; reqIds: readonly string[] }[],
|
|
34
|
+
): SpecMorphology {
|
|
35
|
+
const rules = scenarios.filter((s) => s.classification === 'human');
|
|
36
|
+
const acceptance = scenarios.filter((s) => s.classification === 'executable');
|
|
37
|
+
const acceptanceReqIds = new Set(acceptance.flatMap((s) => [...s.reqIds]));
|
|
34
38
|
const enforced = rules.filter((r) => r.reqIds.some((id) => acceptanceReqIds.has(id)));
|
|
35
39
|
const orphan = acceptance.filter((s) => s.reqIds.length === 0);
|
|
36
40
|
return {
|
|
37
41
|
ruleCount: rules.length,
|
|
38
42
|
ruleEnforcedCount: enforced.length,
|
|
39
|
-
ruleManualCount: rules.filter((r) => r.manual).length,
|
|
40
43
|
rulePendingCount: rules.length - enforced.length,
|
|
41
44
|
acceptanceCount: acceptance.length,
|
|
42
45
|
orphanAcceptanceCount: orphan.length,
|
|
43
46
|
};
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
function morphologyOf(doc: CapabilityDoc): SpecMorphology {
|
|
50
|
+
return morphologyOfScenarios(doc.scenarios);
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
export function collectSpecs(entries: readonly SpecEntry[]): SpecSummary[] {
|
|
47
54
|
return entries.map((e) => {
|
|
48
55
|
const morphology = morphologyOf(e.doc);
|
|
@@ -68,7 +75,7 @@ export function renderSpecsList(summaries: readonly SpecSummary[]): string[] {
|
|
|
68
75
|
for (const s of summaries) {
|
|
69
76
|
const m = s.morphology;
|
|
70
77
|
lines.push(
|
|
71
|
-
` ${pad(s.id, idWidth)}rules ${m.ruleCount} enforced ${m.ruleEnforcedCount}
|
|
78
|
+
` ${pad(s.id, idWidth)}rules ${m.ruleCount} enforced ${m.ruleEnforcedCount} pending ${m.rulePendingCount} acceptance ${m.acceptanceCount}`,
|
|
72
79
|
);
|
|
73
80
|
}
|
|
74
81
|
return lines;
|
package/src/review/review.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { evaluateStaleness, notApplicableStaleness } from '../validation/stalene
|
|
|
6
6
|
*/
|
|
7
7
|
import { validateAllSpecs, type SpecEntry, type SpecIo } from '../validation/validate.ts';
|
|
8
8
|
|
|
9
|
-
export type ReviewKind = 'pending' | '
|
|
9
|
+
export type ReviewKind = 'pending' | 'unbound' | 'stale' | 'locked' | 'validate';
|
|
10
10
|
|
|
11
11
|
export interface ReviewSignal {
|
|
12
12
|
kind: ReviewKind;
|
|
@@ -28,7 +28,7 @@ export interface ReviewInput {
|
|
|
28
28
|
boundChangeCount: number;
|
|
29
29
|
/** Active change summaries for the strict sweep (pending tasks → FAIL). */
|
|
30
30
|
activeChanges?: readonly { name: string; completedTasks: number; totalTasks: number }[];
|
|
31
|
-
/** Restrict per-capability signals (pending/
|
|
31
|
+
/** Restrict per-capability signals (pending/unbound/stale) to this capability. */
|
|
32
32
|
capability?: string;
|
|
33
33
|
/** git + root for real staleness evaluation (v1 parity). */
|
|
34
34
|
git?: GitLike;
|
|
@@ -65,12 +65,10 @@ export function buildReview(input: ReviewInput, io: SpecIo): ReviewResult {
|
|
|
65
65
|
const acceptance = entry.doc.scenarios.filter((s) => s.classification === 'executable');
|
|
66
66
|
const acceptanceReqIds = new Set(acceptance.flatMap((s) => s.reqIds));
|
|
67
67
|
const pending = rules.filter((r) => !r.reqIds.some((id) => acceptanceReqIds.has(id)));
|
|
68
|
-
const manual = rules.filter((r) => r.manual);
|
|
69
68
|
// v1 r5: unbound = orphan acceptance scenarios (no @req link).
|
|
70
69
|
const unbound = acceptance.filter((s) => s.reqIds.length === 0);
|
|
71
70
|
|
|
72
71
|
push('pending', cap, pending.length);
|
|
73
|
-
push('manual', cap, manual.length);
|
|
74
72
|
push('unbound', cap, unbound.length);
|
|
75
73
|
|
|
76
74
|
// staleness (v1 evaluate): real base-ref/scope evaluation.
|
|
@@ -131,7 +129,7 @@ export function buildReview(input: ReviewInput, io: SpecIo): ReviewResult {
|
|
|
131
129
|
|
|
132
130
|
const lines: string[] = [`Review: critical=${criticalCount} warning=${warningCount}`];
|
|
133
131
|
for (const cap of sorted.map((e) => e.doc.header.capability ?? e.fileName)) {
|
|
134
|
-
for (const kind of ['pending', '
|
|
132
|
+
for (const kind of ['pending', 'unbound', 'stale'] as const) {
|
|
135
133
|
const s = signals.find((x) => x.kind === kind && x.capability === cap);
|
|
136
134
|
if (!s) continue;
|
|
137
135
|
lines.push(`${kind}: ${cap} (${s.count})`);
|
package/src/spec/ir.ts
CHANGED
|
@@ -19,7 +19,6 @@ export interface ScenarioIR {
|
|
|
19
19
|
/** `@req:rN` links, normalized to `rN`. */
|
|
20
20
|
reqIds: string[];
|
|
21
21
|
classification: ScenarioClassification;
|
|
22
|
-
manual: boolean;
|
|
23
22
|
/** Rule statement (description lines, trimmed) + step texts for executables. */
|
|
24
23
|
statement: string;
|
|
25
24
|
stepCount: number;
|
package/src/spec/parser.ts
CHANGED
|
@@ -77,20 +77,18 @@ function extractHeader(source: string): CapabilityHeader {
|
|
|
77
77
|
|
|
78
78
|
function classify(tags: string[]): {
|
|
79
79
|
classification: ScenarioIR['classification'];
|
|
80
|
-
manual: boolean;
|
|
81
80
|
errors: SpecStructuralError[];
|
|
82
81
|
} {
|
|
83
82
|
const errors: SpecStructuralError[] = [];
|
|
84
83
|
const has = (t: string): boolean => tags.includes(t);
|
|
85
84
|
const human = has('human');
|
|
86
85
|
const executable = has('executable');
|
|
87
|
-
const manual = has('manual');
|
|
88
86
|
const label = tags.join(',');
|
|
89
87
|
|
|
90
|
-
if (manual
|
|
88
|
+
if (has('manual')) {
|
|
91
89
|
errors.push({
|
|
92
|
-
code: 'tag:manual-
|
|
93
|
-
message: `@manual
|
|
90
|
+
code: 'tag:manual-removed',
|
|
91
|
+
message: `@manual was removed in 0.3.0 — drop the tag (@human already carries the human-judgement semantics) (tags: ${label})`,
|
|
94
92
|
});
|
|
95
93
|
}
|
|
96
94
|
if (human && executable) {
|
|
@@ -104,7 +102,7 @@ function classify(tags: string[]): {
|
|
|
104
102
|
: executable
|
|
105
103
|
? 'executable'
|
|
106
104
|
: 'unclassified';
|
|
107
|
-
return { classification,
|
|
105
|
+
return { classification, errors };
|
|
108
106
|
}
|
|
109
107
|
|
|
110
108
|
/** Parse one capability .feature source into the single-track IR. */
|
|
@@ -165,7 +163,6 @@ export function parseCapability(source: string, fileName = '<inline>'): Capabili
|
|
|
165
163
|
tags,
|
|
166
164
|
reqIds,
|
|
167
165
|
classification: kind.classification,
|
|
168
|
-
manual: kind.manual,
|
|
169
166
|
statement,
|
|
170
167
|
stepCount: stepTexts.length,
|
|
171
168
|
steps,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { readBinding } from '../change/frontmatter.ts';
|
|
1
2
|
/**
|
|
2
3
|
* Change-domain validation (v1 `commands/validate.rs` change path parity):
|
|
3
4
|
* frontmatter/depends_on gates, design/tasks constraints, completeness stage
|
|
4
5
|
* INFO, pattern gate and task gates. IO + git injected (pure).
|
|
5
6
|
*/
|
|
7
|
+
import type { GitLike } from '../git/spawnGit.ts';
|
|
6
8
|
|
|
7
9
|
export type ChangeIssueLevel = 'ERROR' | 'WARNING' | 'INFO';
|
|
8
10
|
|
|
@@ -111,18 +113,21 @@ const COMPLETENESS: Record<string, string> = {
|
|
|
111
113
|
"Change is in 'designed' stage (next: add tasks.md to reach 'planned', then `llman sdd change start` to enter feature branch and reach 'full')",
|
|
112
114
|
planned:
|
|
113
115
|
"Change is in 'planned' stage (next: `llman sdd change start` to enter feature branch and reach 'full')",
|
|
116
|
+
full: "Change is bound and stage is 'full' (verify readiness via `llman sdd show <id> --json` readyToImplement)",
|
|
114
117
|
};
|
|
115
118
|
|
|
116
119
|
/**
|
|
117
120
|
* File-aware change validation with v1 messages/paths (used by the validate
|
|
118
121
|
* command). `strict` escalates WARNING issues to ERROR (v1 build_report).
|
|
122
|
+
* `git` (optional) enables the r63 completeness WARNINGs — Full-not-ready
|
|
123
|
+
* with skill guidance, per change.
|
|
119
124
|
*/
|
|
120
125
|
export function validateChange(
|
|
121
126
|
io: ChangeFsIoLite,
|
|
122
127
|
root: string,
|
|
123
128
|
id: string,
|
|
124
129
|
config: ChangeValidationConfig,
|
|
125
|
-
opts: { stage?: StageGate; strict?: boolean } = {},
|
|
130
|
+
opts: { stage?: StageGate; strict?: boolean; git?: GitLike } = {},
|
|
126
131
|
): ChangeCheckResult {
|
|
127
132
|
const issues: ChangeIssue[] = [];
|
|
128
133
|
const push = (level: ChangeIssueLevel, path: string, message: string): void => {
|
|
@@ -206,7 +211,15 @@ export function validateChange(
|
|
|
206
211
|
|
|
207
212
|
const hasDesign = io.exists(`${dir}design.md`);
|
|
208
213
|
const hasTasks = io.exists(`${dir}tasks.md`);
|
|
209
|
-
const
|
|
214
|
+
const binding = readBinding(text);
|
|
215
|
+
const stage =
|
|
216
|
+
hasDesign && hasTasks
|
|
217
|
+
? binding !== null
|
|
218
|
+
? 'full'
|
|
219
|
+
: 'planned'
|
|
220
|
+
: hasDesign
|
|
221
|
+
? 'designed'
|
|
222
|
+
: 'draft';
|
|
210
223
|
|
|
211
224
|
if (hasTasks && !hasDesign) {
|
|
212
225
|
push(
|
|
@@ -239,6 +252,24 @@ export function validateChange(
|
|
|
239
252
|
|
|
240
253
|
// completeness INFO (v1 surface).
|
|
241
254
|
push('INFO', 'completeness', COMPLETENESS[stage] ?? '');
|
|
255
|
+
|
|
256
|
+
// r63: Full-but-not-ready WARNING with skill guidance (v1 r1 surface).
|
|
257
|
+
if (opts.git !== undefined && binding !== null && stage === 'full') {
|
|
258
|
+
const needs =
|
|
259
|
+
text.match(/^needs_specs_change:\s*(true|false)\s*$/mu)?.[1] !== undefined
|
|
260
|
+
? text.match(/^needs_specs_change:\s*(true|false)\s*$/mu)?.[1] === 'true'
|
|
261
|
+
: true;
|
|
262
|
+
const touched =
|
|
263
|
+
opts.git.runOpt(['diff', '--name-only', `${binding.baseBranch}...${binding.branch}`]) ?? '';
|
|
264
|
+
const landed = touched.includes('llmanspec/specs/');
|
|
265
|
+
if (!landed && needs) {
|
|
266
|
+
push(
|
|
267
|
+
'WARNING',
|
|
268
|
+
'proposal.md',
|
|
269
|
+
`specs not landed: change bound to \`${binding.branch}\` but no changes under \`llmanspec/specs/\` on its bound branch. Edit live specs there and commit (or set \`needs_specs_change: false\` if this change has no live contract edits). Skill: llman-sdd-propose — do NOT re-run change start when already attached; apply only when \`llman-sdd show <id> --json\` reports readyToImplement=true (llman-sdd-apply).`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
242
273
|
}
|
|
243
274
|
|
|
244
275
|
// pattern gate (v1 change-id path).
|
|
@@ -112,8 +112,8 @@ export function validateCapability(
|
|
|
112
112
|
push('ERROR', `${cap}/feature`, 'Feature line must carry a title');
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
// Parser-level structural errors (mutual exclusion,
|
|
116
|
-
// rule scenarios). Header gates and the MUST-word gate are owned here
|
|
115
|
+
// Parser-level structural errors (mutual exclusion, removed-tag migration,
|
|
116
|
+
// nested rule scenarios). Header gates and the MUST-word gate are owned here
|
|
117
117
|
// (mapped below), so the parser's duplicate findings are skipped.
|
|
118
118
|
const OWNED_BY_THIS_LAYER = ['missing-header:', 'rule:missing-must-word'];
|
|
119
119
|
for (const err of doc.errors) {
|
|
@@ -161,6 +161,14 @@ export function validateCapability(
|
|
|
161
161
|
// Dangling acceptance @req links (v1 order: acceptance/@req then coverage).
|
|
162
162
|
const ruleReqIds = new Set(human.flatMap((s) => s.reqIds));
|
|
163
163
|
for (const sc of acceptance) {
|
|
164
|
+
// r65: orphan acceptance scenario — no @req link at all (v1 r132 WARNING).
|
|
165
|
+
if (sc.reqIds.length === 0) {
|
|
166
|
+
push(
|
|
167
|
+
'WARNING',
|
|
168
|
+
`${cap}/acceptance/${sc.name}`,
|
|
169
|
+
`orphan acceptance scenario \`${sc.name}\` has no @req:<req_id> link`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
164
172
|
for (const rid of sc.reqIds) {
|
|
165
173
|
if (!ruleReqIds.has(rid)) {
|
|
166
174
|
push(
|
|
@@ -180,7 +188,7 @@ export function validateCapability(
|
|
|
180
188
|
push(
|
|
181
189
|
'INFO',
|
|
182
190
|
coveragePath(cap),
|
|
183
|
-
`rule ${rid} is pending: no @executable acceptance scenario
|
|
191
|
+
`rule ${rid} is pending: no @executable acceptance scenario`,
|
|
184
192
|
);
|
|
185
193
|
}
|
|
186
194
|
}
|
|
@@ -12,7 +12,7 @@ Every capability `.feature` (`llmanspec/specs/<capability>.feature` or `llmanspe
|
|
|
12
12
|
2) Tag grammar (`@human constraint scenario must carry an @req:<req_id> tag` / `orphan acceptance scenario`):
|
|
13
13
|
- Rules: `@req:<id> @human` — statement in the scenario description (MUST/SHALL required).
|
|
14
14
|
- Acceptance: `@executable` + at least one `@req:<id>` linking a rule.
|
|
15
|
-
|
|
15
|
+
Never combine `@human` with `@executable`. (`@manual` was removed in 0.3.0 — drop it; `@human` already carries the human-judgement semantics.)
|
|
16
16
|
|
|
17
17
|
3) Legacy `spec.toon` present (`legacy spec.toon found ... run ... toon2features`):
|
|
18
18
|
Run `llman-sdd project migrate --kind toon2features --yes`, review the diff, commit.
|
|
@@ -25,5 +25,5 @@ It is the only spec artifact — there is no `spec.toon`.
|
|
|
25
25
|
- Header comments (`# capability:` / `# purpose:` / `# scope:`) are REQUIRED; `scope` drives staleness.
|
|
26
26
|
- `@human` scenarios are human-owned constraints; their description carries the normative statement verbatim. Editing/removing them yields a WARNING only (report-only, never blocks a gate) — compare via git branch diff; the legacy lock-ack metadata `rules_touched` / `agent_acked` / `@agent` is removed with no aliases and no compat layer (locked rules are report-only: a warning, never a block).
|
|
27
27
|
- `@executable` scenarios are runner-bound acceptance; they link rules via `@req:<req_id>`.
|
|
28
|
-
- Coverage tiers: enforced (has acceptance) /
|
|
28
|
+
- Coverage tiers: enforced (has acceptance) / pending. `list --specs` reports both.
|
|
29
29
|
- Scenarios MUST stay top-level: `Rule:` blocks are rejected (the runner skips them silently).
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
2)tag 语法(`@human constraint scenario must carry an @req:<req_id> tag` / `orphan acceptance scenario`):
|
|
13
13
|
- 规则:`@req:<id> @human` —— statement 放场景描述(须含 MUST/SHALL)。
|
|
14
14
|
- 验收:`@executable` 且至少一个 `@req:<id>` 挂到规则。
|
|
15
|
-
-
|
|
15
|
+
- 禁止 `@human` 与 `@executable` 同场景;`@manual` 已在 0.3.0 移除——残留会被报迁移 ERROR,删掉该 tag 即可(`@human` 本身已承载人工判定语义)。
|
|
16
16
|
|
|
17
17
|
3)遗留 `spec.toon`(`legacy spec.toon found ... run ... toon2features`):
|
|
18
18
|
运行 `llman-sdd project migrate --kind toon2features --yes`,审阅 diff 后提交。
|
|
@@ -25,5 +25,5 @@
|
|
|
25
25
|
- 头注释(`# capability:` / `# purpose:` / `# scope:`)必填;`scope` 驱动 staleness 检查。
|
|
26
26
|
- `@human` 场景是人拥有的约束场景;规则 statement 全文放在场景描述里。改/删它只出 WARNING(报告制,不阻断门禁),用 git 分支对比审视;旧的锁定确认元数据 `rules_touched` / `agent_acked` / `@agent` 已删除,无别名也无兼容层。
|
|
27
27
|
- `@executable` 场景是 runner 绑定的验收场景;用 `@req:<req_id>` 挂回规则。
|
|
28
|
-
-
|
|
28
|
+
- 覆盖两态分级:enforced(有验收)/ pending——`list --specs` 逐项输出。
|
|
29
29
|
- 场景 MUST 保持顶层:`Rule:` 块会被拒绝(runner 会静默跳过其中场景)。
|