@dtlabs/ai-toolkit 0.20.0 → 0.21.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/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
## 0.1.4 (2026-07-21)
|
|
2
|
+
## [0.21.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.20.0...v0.21.0) (2026-10-20)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* bound the feature-doc validator to the Tech-Spec skeleton (FTR-028) ([80869ce](https://github.com/dtlab-tech/ai-toolkit/commit/80869ced8b8d24ef7b0669fabbb152e8bf243794))
|
|
8
|
+
* **pm-phase1, agent-registry:** bound the feature-doc validator to the Tech-Spec skeleton (FTR-028) ([9eb7f72](https://github.com/dtlab-tech/ai-toolkit/commit/9eb7f7222ff80eb2b26815a4f58f923f9ca09f1d))
|
|
9
|
+
|
|
2
10
|
## [0.20.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.19.2...v0.20.0) (2026-10-19)
|
|
3
11
|
|
|
4
12
|
|
package/lib/agent-registry.js
CHANGED
|
@@ -259,6 +259,19 @@ const CATALOG = [
|
|
|
259
259
|
deprecationTarget: null,
|
|
260
260
|
platforms: ['claude'],
|
|
261
261
|
},
|
|
262
|
+
{
|
|
263
|
+
agentId: 'gaia.agent.planner.validate-tech-spec-component-detail',
|
|
264
|
+
nativeNames: { claude: 'gaia-validate-tech-spec-component-detail' },
|
|
265
|
+
relativeInstallPath: '.claude/agents/gaia-validate-tech-spec-component-detail.md',
|
|
266
|
+
role: 'planner',
|
|
267
|
+
type: 'validation',
|
|
268
|
+
authorisedPhases: ['planning'],
|
|
269
|
+
allowedPipelines: ['implement-feature'],
|
|
270
|
+
minimumToolkitVersion: '0.13.0',
|
|
271
|
+
deprecated: false,
|
|
272
|
+
deprecationTarget: null,
|
|
273
|
+
platforms: ['claude'],
|
|
274
|
+
},
|
|
262
275
|
{
|
|
263
276
|
agentId: 'gaia.agent.planner.define-feature',
|
|
264
277
|
nativeNames: { claude: 'gaia-define-feature' },
|
package/package.json
CHANGED
|
@@ -16,11 +16,15 @@ A QA agent that cross-references `feature.md` against the Requirements and Tech-
|
|
|
16
16
|
1. Extract the prefix from the folder name (`FTR-001-user-management` → `FTR-001`)
|
|
17
17
|
2. Read `feature.md` fully — its path is given to you first; use your Read tool.
|
|
18
18
|
3. The Requirements and Tech-Spec document text is included directly below, inlined into this
|
|
19
|
-
prompt under `Requirements:` and `Tech-Spec:` labels
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
prompt under `Requirements:` and `Tech-Spec:` labels. The Requirements text is the FULL document
|
|
20
|
+
(every Use Case's own flow detail included). The Tech-Spec text is its SKELETON ONLY — Section
|
|
21
|
+
9's own component index table (Purpose, Addresses, Domain, Spec-link columns, one row per
|
|
22
|
+
component) plus every feature-wide section (Overview, Architecture, Security, Database,
|
|
23
|
+
Configuration, Implementation Order, Risks). It deliberately does NOT include any component's
|
|
24
|
+
own Change/Algorithm/Schema/Tests design prose — that per-component detail is validated
|
|
25
|
+
separately, on request, via `needsComponentDetail` (Phase 3). Do NOT attempt to locate or Read
|
|
26
|
+
`{PREFIX}-Requirements.md`/`{PREFIX}-Tech-Spec.md` or any `spec/components/*` file on disk;
|
|
27
|
+
use the inlined skeleton as the complete Tech-Spec-level input for this pass.
|
|
24
28
|
4. If either inlined document's text is empty or missing, record a finding identifying it, skip
|
|
25
29
|
checks that require it, and complete Phases 6 and 7 with `valid: false`. Never claim full
|
|
26
30
|
coverage when an input could not be read.
|
|
@@ -60,16 +64,26 @@ For each claim extracted in Phase 2, check whether it is addressed in the target
|
|
|
60
64
|
| Every out-of-scope item → Out of Scope section | Listed |
|
|
61
65
|
| Every functional behaviour → at least one Acceptance Criterion | AC in Given/When/Then format |
|
|
62
66
|
|
|
63
|
-
### Tech-Spec coverage (against the inlined Tech-Spec
|
|
67
|
+
### Tech-Spec coverage (against the inlined Tech-Spec SKELETON — Section 9's component table and
|
|
68
|
+
feature-wide sections only; no per-component design body is available in this pass)
|
|
64
69
|
|
|
65
70
|
| Check | Pass condition |
|
|
66
71
|
|-------|---------------|
|
|
67
|
-
| Every
|
|
68
|
-
| Every
|
|
69
|
-
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
| Every new/modified service/component in feature.md → listed in File Inventory | Row present with matching Purpose/Domain |
|
|
73
|
+
| Every external integration → documented in External Integrations (Section 5's own feature-wide prose, if any) | Mentioned explicitly |
|
|
74
|
+
| A claim needs exact technical detail not visible in Purpose/Addresses/Domain (e.g. an endpoint's precise method/path/response shape, a schema field, an error code) | Do NOT guess and do NOT mark it a gap outright. Emit a `needsComponentDetail` entry instead (see below) — never silently drop the check |
|
|
75
|
+
|
|
76
|
+
AC↔component traceability (every Use Case/AC referenced by the right component, and vice versa) is
|
|
77
|
+
NOT this agent's responsibility — it is checked deterministically, by exact ID, by the host after
|
|
78
|
+
this call returns. Do not attempt it here; a prose-based traceability finding in this pass would
|
|
79
|
+
only duplicate or conflict with that exact-ID check.
|
|
80
|
+
|
|
81
|
+
**When to use `needsComponentDetail` vs a normal finding:** if feature.md describes something a
|
|
82
|
+
component row's Purpose plausibly relates to, but you cannot confirm the EXACT technical detail
|
|
83
|
+
without reading that component's own design body, emit a `needsComponentDetail` entry naming that
|
|
84
|
+
row's own Spec-link `fileSlug` and the specific claim — do not guess, and do not silently treat it
|
|
85
|
+
as either covered or missing. If NO component row is even plausibly related to the claim at all,
|
|
86
|
+
that is a normal missing-coverage finding, not a drill-down case.
|
|
73
87
|
|
|
74
88
|
---
|
|
75
89
|
|
|
@@ -154,18 +168,34 @@ Compose `{PREFIX}-Validation-Report.md`'s full content following this template:
|
|
|
154
168
|
Return a single JSON object with exactly these fields, without Markdown fences or explanatory prose:
|
|
155
169
|
|
|
156
170
|
```json
|
|
157
|
-
{"valid": true, "findings": [], "report": "# Validation Report — FTR-001\n\n## Summary\n..."}
|
|
171
|
+
{"valid": true, "findings": [], "report": "# Validation Report — FTR-001\n\n## Summary\n...", "needsComponentDetail": []}
|
|
158
172
|
```
|
|
159
173
|
|
|
160
174
|
For remaining gaps:
|
|
161
175
|
|
|
162
176
|
```json
|
|
163
|
-
{"valid": false, "findings": ["Requirements: UC-01 lacks an acceptance criterion", "Tech-Spec: POST /api/users lacks a response definition"], "report": "# Validation Report — FTR-001\n\n## Summary\n..."}
|
|
177
|
+
{"valid": false, "findings": ["Requirements: UC-01 lacks an acceptance criterion", "Tech-Spec: POST /api/users lacks a response definition"], "report": "# Validation Report — FTR-001\n\n## Summary\n...", "needsComponentDetail": []}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
For a claim needing component-body detail:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{"valid": false, "findings": [], "report": "# Validation Report — FTR-001\n\n## Summary\n...", "needsComponentDetail": [{"claim": "endpoint returns 401 on expired token", "fileSlug": "AuthMiddleware.cs.md"}]}
|
|
164
184
|
```
|
|
165
185
|
|
|
166
186
|
- `valid` is a boolean: `true` only when all required inputs were read, coverage is complete, and no unresolved gaps remain.
|
|
167
187
|
- `findings` is an array of strings, empty only for a clean validation. Missing inputs and ambiguous claims are unresolved findings.
|
|
168
188
|
- `report` is a non-empty string: the complete Phase 6 document content, verbatim, as the host will write it byte-for-byte to `{PREFIX}-Validation-Report.md`.
|
|
189
|
+
- `needsComponentDetail` is an array of `{ claim, fileSlug }` objects — one per claim you could not
|
|
190
|
+
confirm from the Tech-Spec skeleton alone, naming the specific component (by its Spec-link
|
|
191
|
+
`fileSlug`) whose Purpose/Domain plausibly covers it. Empty when every claim was resolvable from
|
|
192
|
+
the skeleton (the common case for small/medium features). An entry here is NOT the same as a
|
|
193
|
+
`findings` entry — do not also add a `findings` string for the same claim; the host resolves
|
|
194
|
+
`needsComponentDetail` entries separately and may add its own finding if the drill-down fails.
|
|
195
|
+
- A `valid: false` result caused only by `needsComponentDetail` entries (no other findings) is
|
|
196
|
+
still `valid: false` with an empty `findings` array — this is expected, not a contradiction: the
|
|
197
|
+
host treats pending component-detail checks as unresolved until its own drill-down pass clears
|
|
198
|
+
them.
|
|
169
199
|
- The caller supplies `--json-schema`; use the runtime's structured-output mechanism to return this object in `structured_output`. Plain-text JSON alone does not satisfy that contract.
|
|
170
200
|
- There is only one deliverable now: the structured verdict, with `report` inside it. There is no separate Write step.
|
|
171
201
|
|
|
@@ -182,4 +212,4 @@ If a claim is contradictory or its correct resolution is ambiguous, do not guess
|
|
|
182
212
|
- **Be specific about gaps** — point to the exact section, claim, and what is missing
|
|
183
213
|
- **Perform one validation pass per invocation** — return remaining gaps to the host
|
|
184
214
|
- **Out-of-scope items are also validated** — ensure they are not accidentally included in outputs
|
|
185
|
-
- **
|
|
215
|
+
- **AC↔component traceability is NOT your job** — the host checks it deterministically, by exact ID, after this call returns (see Phase 3)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gaia-validate-tech-spec-component-detail
|
|
3
|
+
description: "Verifies a small batch of claims against the specific component design files named by the host. Input: a list of {claim, component file path} pairs"
|
|
4
|
+
model: haiku
|
|
5
|
+
tools: Read, Glob, Grep
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Validate Tech-Spec Component Detail
|
|
9
|
+
|
|
10
|
+
A narrow QA agent: for each claim in this batch, Read ONLY the one named component file and decide
|
|
11
|
+
whether its own content satisfies that claim. Do not Read, Glob, or Grep for `feature.md`,
|
|
12
|
+
`req/index.md`, `spec/index.md`, or any component file NOT explicitly named in this prompt — every
|
|
13
|
+
file you need is already named below; searching for anything else is out of scope for this agent
|
|
14
|
+
and wastes the narrow batch this call was sized for.
|
|
15
|
+
|
|
16
|
+
## Phase 1 — Load This Batch
|
|
17
|
+
|
|
18
|
+
The host supplies, in the dispatch prompt, one entry per line:
|
|
19
|
+
`Claim: {claim text}` followed by `Component file: {absolute path}`.
|
|
20
|
+
Multiple claims may name the SAME component file — Read each distinct file only once, then check
|
|
21
|
+
every claim assigned to it against that one reading.
|
|
22
|
+
|
|
23
|
+
## Phase 2 — Verify Each Claim
|
|
24
|
+
|
|
25
|
+
For each claim, Read its named file (if not already read for an earlier claim in this batch) and
|
|
26
|
+
decide: does this file's own actual content (its Change/Algorithm/Schema/Tests sections, or
|
|
27
|
+
whichever of those the file actually has) satisfy the claim? Be specific and literal — a claim
|
|
28
|
+
about an exact endpoint method/path, a schema field, or an error code is satisfied only if that
|
|
29
|
+
exact detail is actually present in the file, not merely plausible given the file's general topic.
|
|
30
|
+
|
|
31
|
+
## Phase 3 — Structured Completion (MANDATORY — your final response)
|
|
32
|
+
|
|
33
|
+
Return a single JSON object with exactly one field:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{"findings": ["Tech-Spec: components/AuthMiddleware.cs.md does not document a 401 response for an expired token, as feature.md requires"]}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- `findings` is an array of strings, one per claim that is NOT satisfied by its named file's
|
|
40
|
+
content. A satisfied claim produces NO entry — do not report successes.
|
|
41
|
+
- Every finding string is prefixed `"Tech-Spec: "`, matching the same convention
|
|
42
|
+
`gaia-validate-feature-docs`'s own `findings` array and the host's deterministic
|
|
43
|
+
AC↔component check already use, so the host can merge every source into one array with no
|
|
44
|
+
format-specific handling for this agent's own output.
|
|
45
|
+
- An empty array (`{"findings": []}`) means every claim in this batch was confirmed.
|
|
46
|
+
- The caller supplies `--json-schema`; use the runtime's structured-output mechanism to return this
|
|
47
|
+
object in `structured_output`.
|
|
48
|
+
|
|
49
|
+
## Guidelines
|
|
50
|
+
|
|
51
|
+
- **Read only the named files** — never search for or infer any other document.
|
|
52
|
+
- **One call, one batch** — do not ask the host for more context; if a named file's content is
|
|
53
|
+
genuinely ambiguous about a claim, report it as NOT satisfied (fail closed) rather than guessing.
|
|
54
|
+
- **Be literal, not generous** — "plausibly related" is not the same as "actually documented".
|
|
@@ -281,6 +281,67 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
281
281
|
return changeIdx !== -1 && testsIdx !== -1
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
// FTR-028/UC-02: purely deterministic, zero-LLM-cost AC<->component traceability check. Reuses
|
|
285
|
+
// the two table parsers already duplicated in this file — no new parsing contract invented.
|
|
286
|
+
// Both directions produce a "Tech-Spec: " finding (always closed by a Tech-Spec revision —
|
|
287
|
+
// either add missing coverage, or fix a bad AC-ID reference).
|
|
288
|
+
function computeAcComponentTraceabilityGaps(reqIndexContent, specIndexContent) {
|
|
289
|
+
const acIds = new Set(parseAcTableRelatedUc(reqIndexContent).map(r => r.id))
|
|
290
|
+
const components = parseComponentsTable(specIndexContent)
|
|
291
|
+
const addressedAcIds = new Set()
|
|
292
|
+
const findings = []
|
|
293
|
+
for (const comp of components) {
|
|
294
|
+
const refs = (comp.addresses || '').split(',').map(s => s.trim()).filter(s => s && s !== '—')
|
|
295
|
+
for (const ref of refs) {
|
|
296
|
+
addressedAcIds.add(ref)
|
|
297
|
+
if (!acIds.has(ref)) findings.push(`Tech-Spec: component ${comp.filePath} references nonexistent ${ref}`)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
for (const id of acIds) {
|
|
301
|
+
if (!addressedAcIds.has(id)) findings.push(`Tech-Spec: ${id} is not addressed by any Tech-Spec component`)
|
|
302
|
+
}
|
|
303
|
+
return findings
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// FTR-028/UC-03: validates each needsComponentDetail entry's fileSlug against the real
|
|
307
|
+
// component table (a hallucinated/mistyped reference is dropped and reported, never
|
|
308
|
+
// dispatched), deduplicates by fileSlug (two claims against the same file collapse into one
|
|
309
|
+
// batch slot), and splits the remaining unique components into groups of at most
|
|
310
|
+
// DRILL_DOWN_BATCH_SIZE — never one call per claim, never one call covering every flagged
|
|
311
|
+
// component regardless of count.
|
|
312
|
+
const DRILL_DOWN_BATCH_SIZE = 8 // fixed constant, not externally configurable
|
|
313
|
+
function prepareDrillDownBatches(needsComponentDetail, specIndexContent) {
|
|
314
|
+
const validSlugs = new Set(parseComponentsTable(specIndexContent).map(c => c.fileSlug))
|
|
315
|
+
const extraFindings = []
|
|
316
|
+
const claimsByFileSlug = new Map()
|
|
317
|
+
for (const entry of needsComponentDetail || []) {
|
|
318
|
+
if (!validSlugs.has(entry.fileSlug)) {
|
|
319
|
+
extraFindings.push(`Tech-Spec: component-detail check referenced a non-existent file "${entry.fileSlug}" for claim "${entry.claim}"`)
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
if (!claimsByFileSlug.has(entry.fileSlug)) claimsByFileSlug.set(entry.fileSlug, [])
|
|
323
|
+
claimsByFileSlug.get(entry.fileSlug).push(entry.claim)
|
|
324
|
+
}
|
|
325
|
+
const fileSlugs = [...claimsByFileSlug.keys()]
|
|
326
|
+
const batches = []
|
|
327
|
+
for (let i = 0; i < fileSlugs.length; i += DRILL_DOWN_BATCH_SIZE) {
|
|
328
|
+
batches.push(fileSlugs.slice(i, i + DRILL_DOWN_BATCH_SIZE)
|
|
329
|
+
.map(slug => ({ fileSlug: slug, claims: claimsByFileSlug.get(slug) })))
|
|
330
|
+
}
|
|
331
|
+
return { batches, extraFindings }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// specDir is already absolute (derived from c.feature()'s realpath-resolved featureDir), so
|
|
335
|
+
// this needs no extra path.resolve — same reasoning that already lets featurePath be handed to
|
|
336
|
+
// agents directly elsewhere in this file.
|
|
337
|
+
function buildDrillDownPrompt(batch) {
|
|
338
|
+
return batch.map(({ fileSlug, claims }) =>
|
|
339
|
+
claims.map(claim => `Claim: ${claim}\nComponent file: ${specDir}/components/${fileSlug}`).join('\n\n')
|
|
340
|
+
).join('\n\n')
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const drillDownId = 'gaia.agent.planner.validate-tech-spec-component-detail'
|
|
344
|
+
|
|
284
345
|
function buildComponentPrompt(row, components, featurePathArg, componentTemplate) {
|
|
285
346
|
const compactIndex = components.map(c => `${c.filePath} (${c.domain})`).join('\n')
|
|
286
347
|
const base = `${featurePathArg}\n\nGenerate the full technical design for this component:\n` +
|
|
@@ -445,26 +506,81 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
445
506
|
// finding mentioning both document names, or neither, would mis-route or
|
|
446
507
|
// silently skip). The validator agent itself decides which document(s) its
|
|
447
508
|
// own findings implicate.
|
|
509
|
+
// FTR-028/UC-01: Tech-Spec text is the raw skeleton (c.read(specIndexPath)), never
|
|
510
|
+
// assembledTechSpec's fully-spliced output — bounds this prompt's size to component COUNT,
|
|
511
|
+
// not per-component prose length. Requirements stays assembledRequirements (full UC
|
|
512
|
+
// detail), unchanged — no real incident has shown UC count x flow length to pose the same
|
|
513
|
+
// risk.
|
|
448
514
|
const result = await c.worker(valId,
|
|
449
|
-
`${featurePath}\n\nRequirements:\n${assembledRequirements}\n\nTech-Spec:\n${
|
|
450
|
-
'Validate only; the host owns the revision loop. Return valid, a findings array (one string per remaining gap), needsRequirementsFix/needsTechSpecFix (true only if at least one finding implicates that document), and the full validation report content. Do not dispatch other agents.',
|
|
451
|
-
{ label: `validation:${cycle}`, outputs: [output('Validation-Report', [featurePath, reqIndexPath, specIndexPath])], schema: { type: 'object', properties: { valid: { type: 'boolean' }, findings: { type: 'array', items: { type: 'string' } }, needsRequirementsFix: { type: 'boolean' }, needsTechSpecFix: { type: 'boolean' }, report: { type: 'string' } }, required: ['valid', 'findings', 'needsRequirementsFix', 'needsTechSpecFix', 'report'] } })
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
515
|
+
`${featurePath}\n\nRequirements:\n${assembledRequirements}\n\nTech-Spec:\n${c.read(specIndexPath)}\n\n` +
|
|
516
|
+
'Validate only; the host owns the revision loop. Return valid, a findings array (one string per remaining gap), needsRequirementsFix/needsTechSpecFix (true only if at least one finding implicates that document), needsComponentDetail (claims needing a component\'s own design body to verify — see your own instructions for when to use this instead of a finding), and the full validation report content. Do not dispatch other agents.',
|
|
517
|
+
{ label: `validation:${cycle}`, outputs: [output('Validation-Report', [featurePath, reqIndexPath, specIndexPath])], schema: { type: 'object', properties: { valid: { type: 'boolean' }, findings: { type: 'array', items: { type: 'string' } }, needsRequirementsFix: { type: 'boolean' }, needsTechSpecFix: { type: 'boolean' }, report: { type: 'string' }, needsComponentDetail: { type: 'array', items: { type: 'object', properties: { claim: { type: 'string' }, fileSlug: { type: 'string' } }, required: ['claim', 'fileSlug'] } } }, required: ['valid', 'findings', 'needsRequirementsFix', 'needsTechSpecFix', 'report'] } })
|
|
518
|
+
// needsComponentDetail is declared in `properties` but deliberately NOT in the schema's
|
|
519
|
+
// top-level `required` array: every pre-existing mocked test response for this agent
|
|
520
|
+
// (predating this feature) omits the field entirely, and making it hard-required would
|
|
521
|
+
// break every one of them for no real gain — prepareDrillDownBatches already treats an
|
|
522
|
+
// absent/undefined value as "no entries" via `needsComponentDetail || []`.
|
|
523
|
+
|
|
524
|
+
// Raw self-consistency guard on the MODEL'S OWN response, before any host-side merge: a
|
|
525
|
+
// pending needsComponentDetail entry is itself a form of "not yet resolved", same as a
|
|
526
|
+
// non-empty findings array — valid:true requires BOTH to be empty, valid:false requires at
|
|
527
|
+
// least one to be non-empty. This is intentionally separate from mergedValid below (which
|
|
528
|
+
// is NOT required to track this raw self-report: a valid:false caused only by
|
|
529
|
+
// needsComponentDetail entries that the drill-down subsequently confirms must still resolve
|
|
530
|
+
// to a clean cycle, not a false "contradiction").
|
|
531
|
+
const rawHasOpenItems = result.findings.length > 0 || (result.needsComponentDetail || []).length > 0
|
|
532
|
+
if (result.valid === rawHasOpenItems) {
|
|
533
|
+
throw new Error(`Validation agent returned a contradictory result on cycle ${cycle}: valid=${result.valid} with ${result.findings.length} finding(s) and ${(result.needsComponentDetail || []).length} needsComponentDetail entry(ies)`)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// FTR-028/UC-02 + UC-03: every finding source for this cycle, computed/dispatched AFTER the
|
|
537
|
+
// main call returns (re-reading the index files fresh, since a prior cycle may have
|
|
538
|
+
// regenerated them). Zero added LLM cost when both come back empty (the common case).
|
|
539
|
+
const reqIndexContent = c.read(reqIndexPath)
|
|
540
|
+
const specIndexContent = c.read(specIndexPath)
|
|
541
|
+
const structuralFindings = computeAcComponentTraceabilityGaps(reqIndexContent, specIndexContent)
|
|
542
|
+
const { batches, extraFindings } = prepareDrillDownBatches(result.needsComponentDetail, specIndexContent)
|
|
543
|
+
|
|
544
|
+
const drillDownFindings = []
|
|
545
|
+
for (let b = 0; b < batches.length; b++) {
|
|
546
|
+
const batchResult = await c.worker(drillDownId, buildDrillDownPrompt(batches[b]),
|
|
547
|
+
{ label: `validation:${cycle}:component-detail:${b + 1}`,
|
|
548
|
+
schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } })
|
|
549
|
+
drillDownFindings.push(...batchResult.findings)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// BR-04: merge everything BEFORE the contradiction guard. A model returning valid:true
|
|
553
|
+
// while the host's own independent checks found a real gap is not a self-contradiction (it
|
|
554
|
+
// was never shown that gap) — it is simply now-invalid, same as any other unresolved
|
|
555
|
+
// finding.
|
|
556
|
+
const newFindings = [...structuralFindings, ...extraFindings, ...drillDownFindings]
|
|
557
|
+
const mergedFindings = [...result.findings, ...newFindings]
|
|
558
|
+
// Deliberately NOT `result.valid && ...`: result.valid may be false SOLELY because of
|
|
559
|
+
// needsComponentDetail entries that the drill-down above has now fully confirmed (zero
|
|
560
|
+
// drillDownFindings) — that must resolve to clean, not stay stuck on the model's own
|
|
561
|
+
// now-stale self-report. The only authoritative signal for the final verdict is whether any
|
|
562
|
+
// finding remains, from any source, after every check this cycle has run.
|
|
563
|
+
const mergedValid = mergedFindings.length === 0
|
|
564
|
+
const mergedNeedsTechSpecFix = result.needsTechSpecFix || newFindings.some(f => f.startsWith('Tech-Spec:'))
|
|
565
|
+
const mergedNeedsRequirementsFix = result.needsRequirementsFix || newFindings.some(f => f.startsWith('Requirements:'))
|
|
566
|
+
|
|
567
|
+
// c.worker() already wrote result.report verbatim via its own outputs[0] contract; only
|
|
568
|
+
// overwrite it when this cycle actually added findings the LLM's own report text doesn't
|
|
569
|
+
// mention — the common clean case (newFindings.length === 0) never re-writes the file.
|
|
570
|
+
if (newFindings.length) {
|
|
571
|
+
c.write(file('Validation-Report'), result.report +
|
|
572
|
+
'\n\n## Additional findings (deterministic + component-detail drill-down)\n' +
|
|
573
|
+
newFindings.map(f => `- ${f}`).join('\n') + '\n')
|
|
458
574
|
}
|
|
459
|
-
if (
|
|
575
|
+
if (mergedValid) {
|
|
460
576
|
if (!fresh(file('Validation-Report'), [featurePath, reqIndexPath, specIndexPath])) throw new Error('Validation agent did not produce its report')
|
|
461
577
|
validationSummary = `0 gaps (clean on cycle ${cycle})`
|
|
462
578
|
break
|
|
463
579
|
}
|
|
464
580
|
if (cycle === 3) throw new Error('Validation gaps remain after 3 cycles')
|
|
465
|
-
if (!
|
|
466
|
-
if (
|
|
467
|
-
if (
|
|
581
|
+
if (!mergedNeedsRequirementsFix && !mergedNeedsTechSpecFix) throw new Error(`Validation agent reported gaps on cycle ${cycle} but flagged neither document for revision`)
|
|
582
|
+
if (mergedNeedsRequirementsFix) { await generateRequirements(true); assembledRequirements = assembleRequirements() }
|
|
583
|
+
if (mergedNeedsTechSpecFix) { await generateTechSpec(true); assembledTechSpec = assembleTechSpec() }
|
|
468
584
|
}
|
|
469
585
|
}
|
|
470
586
|
// Binds Gate 1's eventual approval to the EXACT Requirements/Tech-Spec bytes
|