@llman-sdd/core 0.1.4 → 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/archive/freeze.ts +8 -5
- package/src/change/id.ts +51 -31
- package/src/change/lifecycle.ts +186 -23
- package/src/change/nextId.ts +55 -0
- package/src/change/resolve.ts +34 -0
- package/src/config/changeId.ts +87 -0
- package/src/config/surface.ts +59 -0
- package/src/context/indexStore.ts +41 -0
- package/src/git/spawnGit.ts +12 -0
- package/src/index.ts +50 -1
- package/src/report/collect.ts +30 -14
- package/src/report/graph.ts +339 -43
- package/src/report/show.ts +38 -15
- package/src/report/specHelpers.ts +13 -5
- package/src/report/specs.ts +14 -7
- package/src/review/review.ts +36 -19
- package/src/spec/authoring.ts +206 -0
- package/src/spec/ir.ts +0 -1
- package/src/spec/parser.ts +4 -7
- package/src/validation/changeCheck.ts +341 -0
- package/src/validation/staleness.ts +158 -0
- package/src/validation/validate.ts +125 -42
- 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
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Validation engine (validation capability): aggregates Phase-2 parse errors
|
|
3
|
-
* plus the verdict-equivalent gates
|
|
4
|
-
* filesystem access is
|
|
3
|
+
* plus the verdict-equivalent gates (r11/r12, ordered to match v1
|
|
4
|
+
* `spec/validation.rs` observable issue order). Pure — filesystem access is
|
|
5
|
+
* injected via SpecIo.
|
|
5
6
|
*/
|
|
6
7
|
import type { CapabilityDoc } from '../spec/ir.ts';
|
|
7
8
|
import { MUST_WORD_RE } from '../spec/ir.ts';
|
|
@@ -11,7 +12,7 @@ export type ValidationLevel = 'ERROR' | 'WARNING' | 'INFO';
|
|
|
11
12
|
|
|
12
13
|
export interface ValidationItem {
|
|
13
14
|
level: ValidationLevel;
|
|
14
|
-
/** Gate anchor, e.g. `t/rule/ok` or `t/valid_scope` (v1-style). */
|
|
15
|
+
/** Gate anchor, e.g. `t/rule/ok` or `t/valid_scope` (v1-style `path`). */
|
|
15
16
|
id: string;
|
|
16
17
|
message: string;
|
|
17
18
|
}
|
|
@@ -39,79 +40,156 @@ export interface ValidationReport {
|
|
|
39
40
|
failed: boolean;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
const rulesPath = (cap: string): string => `${cap}/rules`;
|
|
44
|
+
const acceptanceReqPath = (cap: string, name: string): string => `${cap}/acceptance/${name}/@req`;
|
|
45
|
+
const coveragePath = (cap: string): string => `${cap}/coverage`;
|
|
46
|
+
|
|
42
47
|
export function validateCapability(
|
|
43
48
|
entry: SpecEntry,
|
|
44
49
|
duplicatesFor: (reqId: string) => boolean,
|
|
45
50
|
io: SpecIo,
|
|
51
|
+
opts: { strict?: boolean } = {},
|
|
46
52
|
): SpecVerdict {
|
|
47
53
|
const { doc } = entry;
|
|
48
54
|
const cap = doc.header.capability ?? entry.fileName;
|
|
55
|
+
const strict = opts.strict === true;
|
|
49
56
|
const items: ValidationItem[] = [];
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
const push = (level: ValidationLevel, id: string, message: string): void => {
|
|
59
|
+
items.push({ level, id, message });
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Header gates (r12 / v1 spec_meta). v1 treats a missing `# capability:`
|
|
63
|
+
// header as a parse-level failure: only `file` + registry-scan issues are
|
|
64
|
+
// emitted and all single-track gates are skipped.
|
|
53
65
|
if (doc.header.capability === null) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
});
|
|
66
|
+
const msg = `spec \`${cap}\`: missing \`# capability:\` header comment (spec-format r133)`;
|
|
67
|
+
push('ERROR', 'file', msg);
|
|
68
|
+
push('ERROR', 'llmanspec/specs', `Failed to scan req_id index: ${msg}`);
|
|
69
|
+
return { fileName: entry.fileName, capability: cap, ok: false, items };
|
|
59
70
|
}
|
|
60
|
-
if (doc.header.purpose === null) {
|
|
61
|
-
|
|
71
|
+
if (doc.header.purpose === null || doc.header.purpose.trim() === '') {
|
|
72
|
+
push(
|
|
73
|
+
'ERROR',
|
|
74
|
+
`${cap}/purpose`,
|
|
75
|
+
'`# purpose:` header comment must not be empty (spec-format r133)',
|
|
76
|
+
);
|
|
62
77
|
}
|
|
63
78
|
if (doc.header.scope === null) {
|
|
64
|
-
|
|
79
|
+
push(
|
|
80
|
+
'ERROR',
|
|
81
|
+
`${cap}/valid_scope`,
|
|
82
|
+
'Spec valid_scope must not be empty (add it inside the .toon document).',
|
|
83
|
+
);
|
|
65
84
|
} else {
|
|
66
|
-
|
|
85
|
+
// r42: missing valid_scope paths are independent failures —
|
|
86
|
+
// ERROR (nonzero exit) under --strict, WARNING otherwise.
|
|
87
|
+
const missing = doc.header.scope
|
|
67
88
|
.split(',')
|
|
68
89
|
.map((s) => s.trim())
|
|
69
|
-
.filter((s) => s !== '')
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
90
|
+
.filter((s) => s !== '')
|
|
91
|
+
.filter((s) => !io.exists(s));
|
|
92
|
+
if (missing.length > 0) {
|
|
93
|
+
push(
|
|
94
|
+
strict ? 'ERROR' : 'WARNING',
|
|
95
|
+
`${cap}/valid_scope`,
|
|
96
|
+
`valid_scope path(s) do not exist on disk: ${missing.join(', ')}`,
|
|
97
|
+
);
|
|
77
98
|
}
|
|
78
99
|
}
|
|
79
100
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
101
|
+
// Spec name must match the capability id (flat stem).
|
|
102
|
+
if (doc.header.capability !== null && doc.header.capability.trim() !== cap) {
|
|
103
|
+
push(
|
|
104
|
+
'WARNING',
|
|
105
|
+
`${cap}/meta.name`,
|
|
106
|
+
`Spec \`# capability:\` header must match spec id (flat file stem or directory name): \`${doc.header.capability.trim()}\` != \`${cap}\``,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Feature title must be non-empty.
|
|
111
|
+
if ((doc.featureName ?? '').trim() === '') {
|
|
112
|
+
push('ERROR', `${cap}/feature`, 'Feature line must carry a title');
|
|
113
|
+
}
|
|
114
|
+
|
|
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
|
+
// (mapped below), so the parser's duplicate findings are skipped.
|
|
84
118
|
const OWNED_BY_THIS_LAYER = ['missing-header:', 'rule:missing-must-word'];
|
|
85
119
|
for (const err of doc.errors) {
|
|
86
120
|
if (OWNED_BY_THIS_LAYER.some((prefix) => err.code.startsWith(prefix))) continue;
|
|
87
|
-
|
|
121
|
+
push('ERROR', err.code.startsWith('file') ? 'file' : `${cap}/${err.code}`, err.message);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Single-track gates (v1 validate_single_track order).
|
|
125
|
+
const human = doc.scenarios.filter((s) => s.classification === 'human');
|
|
126
|
+
const acceptance = doc.scenarios.filter((s) => s.classification === 'executable');
|
|
127
|
+
|
|
128
|
+
if (human.length === 0) {
|
|
129
|
+
push('ERROR', rulesPath(cap), 'spec must define at least one @human constraint scenario');
|
|
88
130
|
}
|
|
89
131
|
|
|
90
132
|
for (const scenario of doc.scenarios) {
|
|
91
133
|
const anchor = `${cap}/rule/${scenario.name}`;
|
|
92
134
|
if (scenario.classification === 'human') {
|
|
93
135
|
if (scenario.reqIds.length === 0) {
|
|
94
|
-
|
|
95
|
-
level: 'ERROR',
|
|
96
|
-
id: anchor,
|
|
97
|
-
message: '@human constraint scenario must carry an @req:<req_id> tag',
|
|
98
|
-
});
|
|
136
|
+
push('ERROR', anchor, '@human constraint scenario must carry an @req:<req_id> tag');
|
|
99
137
|
}
|
|
100
138
|
if (!MUST_WORD_RE.test(scenario.statement)) {
|
|
101
|
-
|
|
102
|
-
level: 'ERROR',
|
|
103
|
-
id: anchor,
|
|
104
|
-
message: 'constraint statement must contain MUST/SHALL (or 必须/不得/禁止)',
|
|
105
|
-
});
|
|
139
|
+
push('ERROR', anchor, 'constraint statement must contain MUST/SHALL (or 必须/不得/禁止)');
|
|
106
140
|
}
|
|
141
|
+
} else if (scenario.classification === 'executable') {
|
|
142
|
+
// (pairing handled below, after all rule req ids are known)
|
|
143
|
+
} else {
|
|
144
|
+
push(
|
|
145
|
+
'WARNING',
|
|
146
|
+
`${cap}/scenario/${scenario.name}`,
|
|
147
|
+
`scenario \`${scenario.name}\` carries neither @human nor @executable; tag it or drop it`,
|
|
148
|
+
);
|
|
107
149
|
}
|
|
108
150
|
for (const reqId of scenario.reqIds) {
|
|
109
151
|
if (duplicatesFor(reqId)) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
152
|
+
push(
|
|
153
|
+
'ERROR',
|
|
154
|
+
`${cap}/registry/${reqId}`,
|
|
155
|
+
`global duplicate req_id \`${reqId}\` used by multiple capabilities`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Dangling acceptance @req links (v1 order: acceptance/@req then coverage).
|
|
162
|
+
const ruleReqIds = new Set(human.flatMap((s) => s.reqIds));
|
|
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
|
+
}
|
|
172
|
+
for (const rid of sc.reqIds) {
|
|
173
|
+
if (!ruleReqIds.has(rid)) {
|
|
174
|
+
push(
|
|
175
|
+
'ERROR',
|
|
176
|
+
acceptanceReqPath(cap, sc.name),
|
|
177
|
+
`@req:${rid} on acceptance scenario \`${sc.name}\` has no matching @human constraint`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Rule coverage INFO (r134 pending rules).
|
|
184
|
+
for (const sc of human) {
|
|
185
|
+
for (const rid of sc.reqIds) {
|
|
186
|
+
const covered = acceptance.some((a) => a.reqIds.includes(rid));
|
|
187
|
+
if (!covered) {
|
|
188
|
+
push(
|
|
189
|
+
'INFO',
|
|
190
|
+
coveragePath(cap),
|
|
191
|
+
`rule ${rid} is pending: no @executable acceptance scenario`,
|
|
192
|
+
);
|
|
115
193
|
}
|
|
116
194
|
}
|
|
117
195
|
}
|
|
@@ -149,3 +227,8 @@ export function validateAllSpecs(entries: readonly SpecEntry[], io: SpecIo): Val
|
|
|
149
227
|
|
|
150
228
|
return { verdicts, lines, failed };
|
|
151
229
|
}
|
|
230
|
+
|
|
231
|
+
/** v1 `apply_strict`: WARNING issues escalate to ERROR when --strict. */
|
|
232
|
+
export function applyStrict<T extends { level: ValidationLevel }>(items: readonly T[]): T[] {
|
|
233
|
+
return items.map((i) => (i.level === 'WARNING' ? { ...i, level: 'ERROR' as const } : i));
|
|
234
|
+
}
|
|
@@ -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 会静默跳过其中场景)。
|