frontend-harness 0.7.10 → 0.8.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/README.md +17 -10
- package/dist/cli/index.js +368 -65
- package/dist/cli/index.js.map +1 -1
- package/dist/runtime/builtin-skills.js +1 -1
- package/dist/runtime/clean.js +1 -0
- package/dist/runtime/clean.js.map +1 -1
- package/dist/runtime/command-taxonomy.js +6 -1
- package/dist/runtime/command-taxonomy.js.map +1 -1
- package/dist/runtime/context.d.ts +33 -2
- package/dist/runtime/context.js +63 -8
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/evidence.d.ts +27 -0
- package/dist/runtime/evidence.js +240 -12
- package/dist/runtime/evidence.js.map +1 -1
- package/dist/runtime/ingest.d.ts +38 -0
- package/dist/runtime/ingest.js +157 -0
- package/dist/runtime/ingest.js.map +1 -0
- package/dist/runtime/knowledge/constants.js +1 -0
- package/dist/runtime/knowledge/constants.js.map +1 -1
- package/dist/runtime/knowledge/core.d.ts +7 -1
- package/dist/runtime/knowledge/core.js +329 -24
- package/dist/runtime/knowledge/core.js.map +1 -1
- package/dist/runtime/knowledge/types.d.ts +70 -1
- package/dist/runtime/knowledge.d.ts +2 -2
- package/dist/runtime/knowledge.js +1 -1
- package/dist/runtime/knowledge.js.map +1 -1
- package/dist/runtime/plan/guidance.d.ts +1 -1
- package/dist/runtime/plan/guidance.js +88 -25
- package/dist/runtime/plan/guidance.js.map +1 -1
- package/dist/runtime/plan/workflow.d.ts +1 -0
- package/dist/runtime/plan/workflow.js +32 -1
- package/dist/runtime/plan/workflow.js.map +1 -1
- package/dist/runtime/plan.d.ts +13 -0
- package/dist/runtime/plan.js +132 -79
- package/dist/runtime/plan.js.map +1 -1
- package/dist/runtime/policy-provenance.d.ts +17 -1
- package/dist/runtime/policy-provenance.js +42 -12
- package/dist/runtime/policy-provenance.js.map +1 -1
- package/dist/runtime/protocol-init.js +12 -7
- package/dist/runtime/protocol-init.js.map +1 -1
- package/dist/runtime/repair-packet.js +86 -4
- package/dist/runtime/repair-packet.js.map +1 -1
- package/dist/runtime/skills.d.ts +9 -0
- package/dist/runtime/skills.js +13 -0
- package/dist/runtime/skills.js.map +1 -1
- package/dist/runtime/state-explain.js +39 -7
- package/dist/runtime/state-explain.js.map +1 -1
- package/dist/runtime/state.js +36 -2
- package/dist/runtime/state.js.map +1 -1
- package/dist/runtime/task-context.d.ts +122 -0
- package/dist/runtime/task-context.js +1322 -0
- package/dist/runtime/task-context.js.map +1 -0
- package/dist/runtime/task-signals.d.ts +1 -0
- package/dist/runtime/task-signals.js +1 -1
- package/dist/runtime/task-signals.js.map +1 -1
- package/dist/runtime/ui-restoration.d.ts +3 -3
- package/dist/runtime/ui-restoration.js +3 -3
- package/dist/runtime/ui-restoration.js.map +1 -1
- package/dist/runtime/ui-source.d.ts +6 -0
- package/dist/runtime/ui-source.js +66 -0
- package/dist/runtime/ui-source.js.map +1 -0
- package/dist/runtime/verify.js +65 -25
- package/dist/runtime/verify.js.map +1 -1
- package/dist/schemas/types.d.ts +75 -2
- package/dist/schemas/validation.js +42 -7
- package/dist/schemas/validation.js.map +1 -1
- package/docs/DIRECTION.md +13 -11
- package/package.json +1 -1
|
@@ -3,7 +3,14 @@ import path from "node:path";
|
|
|
3
3
|
import { isDirectoryWithoutSymlink, isRegularFileWithoutSymlink, writeJson, writeText } from "../../storage/json.js";
|
|
4
4
|
import { harnessPath, relativeHarnessPath } from "../../storage/paths.js";
|
|
5
5
|
import { normalizeProjectRelativePath } from "../project-paths.js";
|
|
6
|
+
import { showIngestRecord } from "../ingest.js";
|
|
6
7
|
import { kindTypeMap, knowledgeKinds, knowledgeStabilities, knowledgeStatuses, knowledgeTypes, MAX_KNOWLEDGE_DIRECTORY_DEPTH, MAX_KNOWLEDGE_MARKDOWN_BYTES, MAX_KNOWLEDGE_MARKDOWN_FILES } from "./constants.js";
|
|
8
|
+
export const MAX_KNOWLEDGE_LIST_CARDS = 20;
|
|
9
|
+
export const MAX_KNOWLEDGE_SEARCH_RESULTS = 8;
|
|
10
|
+
export const MAX_KNOWLEDGE_DRAFT_UPDATE_CANDIDATES = 8;
|
|
11
|
+
export const MAX_KNOWLEDGE_INDEX_CARDS = 20;
|
|
12
|
+
export const MAX_KNOWLEDGE_INDEX_DETAIL_ITEMS = 20;
|
|
13
|
+
const MAX_KNOWLEDGE_REFERENCE_SUGGESTIONS = 20;
|
|
7
14
|
export function promoteKnowledge(projectRoot, input) {
|
|
8
15
|
const title = input.title?.trim();
|
|
9
16
|
const body = input.body?.trim();
|
|
@@ -121,11 +128,13 @@ export function addKnowledge(projectRoot, input) {
|
|
|
121
128
|
const contentFields = atomicContentFields(kind, input);
|
|
122
129
|
const inputSummary = input.summary?.trim();
|
|
123
130
|
const contentSummary = inputSummary || summarizeAtomicContent(kind, subject, contentFields);
|
|
131
|
+
const sourcePaths = parseKnowledgeSourcePaths(input.source, "knowledge add --source");
|
|
132
|
+
const type = typeForAtomicKnowledge(kind, sourcePaths);
|
|
124
133
|
const rendered = renderAtomicKnowledge({
|
|
125
134
|
id,
|
|
126
135
|
title,
|
|
127
136
|
summary: contentSummary,
|
|
128
|
-
type
|
|
137
|
+
type,
|
|
129
138
|
kind,
|
|
130
139
|
subject,
|
|
131
140
|
status,
|
|
@@ -133,7 +142,7 @@ export function addKnowledge(projectRoot, input) {
|
|
|
133
142
|
createdAt,
|
|
134
143
|
scope: splitList(input.scope),
|
|
135
144
|
tags: splitList(input.tags),
|
|
136
|
-
sourcePaths
|
|
145
|
+
sourcePaths,
|
|
137
146
|
verification: splitList(input.verification),
|
|
138
147
|
coverage: splitList(input.coverage),
|
|
139
148
|
prdAnchors: splitList(input.anchor),
|
|
@@ -144,12 +153,184 @@ export function addKnowledge(projectRoot, input) {
|
|
|
144
153
|
path: relativePath,
|
|
145
154
|
id,
|
|
146
155
|
title,
|
|
147
|
-
type
|
|
156
|
+
type,
|
|
148
157
|
status,
|
|
149
158
|
stability,
|
|
150
159
|
createdAt
|
|
151
160
|
};
|
|
152
161
|
}
|
|
162
|
+
export function draftKnowledgeFromIngest(projectRoot, input) {
|
|
163
|
+
if (!input.ref?.trim()) {
|
|
164
|
+
throw new Error("knowledge draft requires --ref ingest://<kind>/<source-id>");
|
|
165
|
+
}
|
|
166
|
+
const record = showIngestRecord(projectRoot, input.ref);
|
|
167
|
+
const recommendedKind = recommendedKnowledgeKind(record.kind);
|
|
168
|
+
const recommendedType = typeForAtomicKnowledge(recommendedKind, [record.ingestRef]);
|
|
169
|
+
const subject = draftSubject(record);
|
|
170
|
+
const id = slugify(`${subject}-${recommendedKind}`) || `${record.id}-${recommendedKind}`;
|
|
171
|
+
const defaults = {
|
|
172
|
+
id,
|
|
173
|
+
kind: recommendedKind,
|
|
174
|
+
subject,
|
|
175
|
+
summary: record.summary,
|
|
176
|
+
source: record.ingestRef,
|
|
177
|
+
coverage: draftCoveragePlaceholder(record.kind),
|
|
178
|
+
verification: draftVerificationPlaceholder(record.kind),
|
|
179
|
+
scope: draftScope(record),
|
|
180
|
+
tags: draftTags(record.kind)
|
|
181
|
+
};
|
|
182
|
+
const contentFlag = contentFlagForKind(recommendedKind);
|
|
183
|
+
const updateCandidateSelection = draftUpdateCandidates(projectRoot, record, defaults);
|
|
184
|
+
const updateCandidates = updateCandidateSelection.candidates;
|
|
185
|
+
const args = [
|
|
186
|
+
"knowledge",
|
|
187
|
+
"add",
|
|
188
|
+
"--json",
|
|
189
|
+
"--id",
|
|
190
|
+
defaults.id,
|
|
191
|
+
"--kind",
|
|
192
|
+
defaults.kind,
|
|
193
|
+
"--subject",
|
|
194
|
+
defaults.subject,
|
|
195
|
+
"--summary",
|
|
196
|
+
defaults.summary,
|
|
197
|
+
`--${contentFlag}`,
|
|
198
|
+
`<atomic ${contentFlag} distilled from ${record.ingestRef}>`,
|
|
199
|
+
"--source",
|
|
200
|
+
defaults.source,
|
|
201
|
+
"--coverage",
|
|
202
|
+
defaults.coverage,
|
|
203
|
+
"--verification",
|
|
204
|
+
defaults.verification,
|
|
205
|
+
"--scope",
|
|
206
|
+
defaults.scope,
|
|
207
|
+
"--tags",
|
|
208
|
+
defaults.tags
|
|
209
|
+
];
|
|
210
|
+
return {
|
|
211
|
+
contractVersion: 1,
|
|
212
|
+
ingestRef: record.ingestRef,
|
|
213
|
+
ingestKind: record.kind,
|
|
214
|
+
source: record.source.value,
|
|
215
|
+
summary: record.summary,
|
|
216
|
+
recommendedKind,
|
|
217
|
+
recommendedType,
|
|
218
|
+
recommendedAction: updateCandidates.length > 0 ? "update" : "add",
|
|
219
|
+
command: "frontend-harness knowledge add --json",
|
|
220
|
+
args,
|
|
221
|
+
updateCandidates,
|
|
222
|
+
limits: {
|
|
223
|
+
updateCandidates: updateCandidateSelection.limits
|
|
224
|
+
},
|
|
225
|
+
requiredEdits: [
|
|
226
|
+
...(updateCandidateSelection.limits.total > 0
|
|
227
|
+
? ["Prefer a matching update candidate before adding a new card for the same source."]
|
|
228
|
+
: []),
|
|
229
|
+
`Replace --${contentFlag} with one atomic durable fact; create separate cards for separate facts.`,
|
|
230
|
+
"Replace --coverage with the precise source section, endpoint, screen, or acceptance block.",
|
|
231
|
+
"Replace --verification with the concrete check or evidence that proves the fact still holds.",
|
|
232
|
+
"Narrow --scope and --tags so future context selection can find the card without broad matching."
|
|
233
|
+
],
|
|
234
|
+
defaults
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function draftUpdateCandidates(projectRoot, record, defaults) {
|
|
238
|
+
const producedIds = new Set(record.producedKnowledgeCardIds);
|
|
239
|
+
const cards = discoverKnowledgeCards(projectRoot);
|
|
240
|
+
const matches = cards
|
|
241
|
+
.filter((card) => producedIds.has(card.id) || card.sourcePaths.some((sourcePath) => sameKnowledgeSourceReference(sourcePath, record.ingestRef) || sameKnowledgeSourceReference(sourcePath, record.source.value)))
|
|
242
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
243
|
+
const selected = matches
|
|
244
|
+
.slice(0, MAX_KNOWLEDGE_DRAFT_UPDATE_CANDIDATES)
|
|
245
|
+
.map((card) => ({
|
|
246
|
+
id: card.id,
|
|
247
|
+
path: card.path,
|
|
248
|
+
title: card.title,
|
|
249
|
+
summary: card.summary,
|
|
250
|
+
sourcePaths: card.sourcePaths,
|
|
251
|
+
command: "frontend-harness knowledge update --json",
|
|
252
|
+
args: [
|
|
253
|
+
"knowledge",
|
|
254
|
+
"update",
|
|
255
|
+
"--json",
|
|
256
|
+
"--id",
|
|
257
|
+
card.id,
|
|
258
|
+
"--summary",
|
|
259
|
+
defaults.summary,
|
|
260
|
+
"--source",
|
|
261
|
+
defaults.source,
|
|
262
|
+
"--coverage",
|
|
263
|
+
defaults.coverage,
|
|
264
|
+
"--verification",
|
|
265
|
+
defaults.verification,
|
|
266
|
+
"--scope",
|
|
267
|
+
defaults.scope,
|
|
268
|
+
"--tags",
|
|
269
|
+
defaults.tags
|
|
270
|
+
]
|
|
271
|
+
}));
|
|
272
|
+
return {
|
|
273
|
+
candidates: selected,
|
|
274
|
+
limits: {
|
|
275
|
+
selected: selected.length,
|
|
276
|
+
total: matches.length,
|
|
277
|
+
omitted: Math.max(0, matches.length - selected.length)
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function recommendedKnowledgeKind(kind) {
|
|
282
|
+
if (kind === "openapi") {
|
|
283
|
+
return "workflow";
|
|
284
|
+
}
|
|
285
|
+
return "rule";
|
|
286
|
+
}
|
|
287
|
+
function typeForAtomicKnowledge(kind, sourcePaths) {
|
|
288
|
+
if (sourcePaths.some(isApiContractSourcePath)) {
|
|
289
|
+
return "api_contract";
|
|
290
|
+
}
|
|
291
|
+
return kindTypeMap[kind];
|
|
292
|
+
}
|
|
293
|
+
function isApiContractSourcePath(sourcePath) {
|
|
294
|
+
const normalized = sourcePath.replace(/\\/g, "/");
|
|
295
|
+
if (/^ingest:\/\/openapi\//i.test(normalized)) {
|
|
296
|
+
return isIngestReference(normalized);
|
|
297
|
+
}
|
|
298
|
+
if (/(^|\/)(openapi|swagger)(\/|[-_.])/i.test(normalized)) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
return /(^|\/)api\/[^/]+\.(?:json|ya?ml)$/i.test(normalized);
|
|
302
|
+
}
|
|
303
|
+
function draftSubject(record) {
|
|
304
|
+
const sourceName = path.basename(record.source.value).replace(/\.[^.]+$/, "");
|
|
305
|
+
const raw = sourceName || record.id;
|
|
306
|
+
return titleCase(raw.replace(/[-_.]+/g, " ")).trim() || record.id;
|
|
307
|
+
}
|
|
308
|
+
function draftScope(record) {
|
|
309
|
+
return record.id.replace(/[^a-z0-9-]+/gi, "-").toLowerCase();
|
|
310
|
+
}
|
|
311
|
+
function draftCoveragePlaceholder(kind) {
|
|
312
|
+
if (kind === "ui-package") {
|
|
313
|
+
return "<screen, component, state, or visual reference>";
|
|
314
|
+
}
|
|
315
|
+
if (kind === "openapi") {
|
|
316
|
+
return "<endpoint, schema, operation, or contract behavior>";
|
|
317
|
+
}
|
|
318
|
+
return "<precise section, endpoint, screen, or acceptance block>";
|
|
319
|
+
}
|
|
320
|
+
function draftVerificationPlaceholder(kind) {
|
|
321
|
+
if (kind === "ui-package") {
|
|
322
|
+
return "<visual, responsive, or interaction evidence required>";
|
|
323
|
+
}
|
|
324
|
+
if (kind === "openapi") {
|
|
325
|
+
return "<contract shape check or mocked integration evidence required>";
|
|
326
|
+
}
|
|
327
|
+
return "<specific check or evidence required>";
|
|
328
|
+
}
|
|
329
|
+
function draftTags(kind) {
|
|
330
|
+
return kind === "ui-package"
|
|
331
|
+
? "ui-package,ui"
|
|
332
|
+
: kind.replace(/[^a-z0-9-]+/gi, "-").toLowerCase();
|
|
333
|
+
}
|
|
153
334
|
export function updateKnowledge(projectRoot, input) {
|
|
154
335
|
const target = input.id?.trim() || input.path?.trim();
|
|
155
336
|
if (!target) {
|
|
@@ -231,6 +412,7 @@ export function indexKnowledge(projectRoot) {
|
|
|
231
412
|
const cards = discoverKnowledgeCards(projectRoot);
|
|
232
413
|
const prdSources = summarizePrdSources(projectRoot, cards);
|
|
233
414
|
const moduleSummaries = summarizeModules(cards);
|
|
415
|
+
const selectedCards = cards.slice(0, MAX_KNOWLEDGE_INDEX_CARDS);
|
|
234
416
|
const result = {
|
|
235
417
|
artifactPath: relativeHarnessPath("knowledge", "index.json"),
|
|
236
418
|
cardCount: cards.length,
|
|
@@ -242,7 +424,14 @@ export function indexKnowledge(projectRoot) {
|
|
|
242
424
|
byKind: countByKind(cards),
|
|
243
425
|
moduleSummaries,
|
|
244
426
|
prdSources,
|
|
245
|
-
cards:
|
|
427
|
+
cards: selectedCards.map(stripSearchable),
|
|
428
|
+
limits: {
|
|
429
|
+
cards: {
|
|
430
|
+
selected: selectedCards.length,
|
|
431
|
+
total: cards.length,
|
|
432
|
+
omitted: Math.max(0, cards.length - selectedCards.length)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
246
435
|
};
|
|
247
436
|
writeJson(path.join(projectRoot, result.artifactPath), result);
|
|
248
437
|
return result;
|
|
@@ -270,7 +459,7 @@ export function checkKnowledgeCoverage(projectRoot, options = {}) {
|
|
|
270
459
|
.filter((source) => source.activeCardCount === 0)
|
|
271
460
|
.map((source) => source.path);
|
|
272
461
|
const missingSourcePaths = [...new Set(cards.flatMap((card) => card.sourcePaths)
|
|
273
|
-
.filter((sourcePath) => sourcePath && !isExternalReference(sourcePath) && !
|
|
462
|
+
.filter((sourcePath) => sourcePath && !isExternalReference(sourcePath) && !isResolvedKnowledgeSource(projectRoot, sourcePath)))].sort();
|
|
274
463
|
const cardsWithoutSource = cards
|
|
275
464
|
.filter((card) => card.status === "active" && card.type === "prd_semantics" && card.sourcePaths.length === 0)
|
|
276
465
|
.map((card) => card.path);
|
|
@@ -320,15 +509,23 @@ export function listKnowledge(projectRoot, options = {}) {
|
|
|
320
509
|
const cards = discoverKnowledgeCards(projectRoot)
|
|
321
510
|
.filter((card) => options.includeDeprecated || card.status === "active")
|
|
322
511
|
.map(stripSearchable);
|
|
323
|
-
|
|
512
|
+
const selected = cards.slice(0, MAX_KNOWLEDGE_LIST_CARDS);
|
|
513
|
+
return {
|
|
514
|
+
cards: selected,
|
|
515
|
+
limits: {
|
|
516
|
+
selected: selected.length,
|
|
517
|
+
total: cards.length,
|
|
518
|
+
omitted: Math.max(0, cards.length - selected.length)
|
|
519
|
+
}
|
|
520
|
+
};
|
|
324
521
|
}
|
|
325
522
|
export function searchKnowledge(projectRoot, query, options = {}) {
|
|
326
523
|
const normalizedQuery = normalize(query ?? "");
|
|
327
524
|
if (!normalizedQuery) {
|
|
328
525
|
throw new Error("knowledge search requires --query");
|
|
329
526
|
}
|
|
330
|
-
const limit = options.limit && options.limit > 0 ? options.limit :
|
|
331
|
-
const
|
|
527
|
+
const limit = options.limit && options.limit > 0 ? options.limit : MAX_KNOWLEDGE_SEARCH_RESULTS;
|
|
528
|
+
const matches = discoverKnowledgeCards(projectRoot)
|
|
332
529
|
.filter((card) => options.includeDeprecated || card.status === "active")
|
|
333
530
|
.map((card) => ({
|
|
334
531
|
card,
|
|
@@ -340,13 +537,22 @@ export function searchKnowledge(projectRoot, query, options = {}) {
|
|
|
340
537
|
return right.score - left.score;
|
|
341
538
|
}
|
|
342
539
|
return left.card.path.localeCompare(right.card.path);
|
|
343
|
-
})
|
|
540
|
+
});
|
|
541
|
+
const cards = matches
|
|
344
542
|
.slice(0, limit)
|
|
345
543
|
.map(({ card, score }) => ({
|
|
346
544
|
...stripSearchable(card),
|
|
347
545
|
score
|
|
348
546
|
}));
|
|
349
|
-
return {
|
|
547
|
+
return {
|
|
548
|
+
query: query ?? "",
|
|
549
|
+
cards,
|
|
550
|
+
limits: {
|
|
551
|
+
selected: cards.length,
|
|
552
|
+
total: matches.length,
|
|
553
|
+
omitted: Math.max(0, matches.length - cards.length)
|
|
554
|
+
}
|
|
555
|
+
};
|
|
350
556
|
}
|
|
351
557
|
export function showKnowledge(projectRoot, idOrPath) {
|
|
352
558
|
const target = idOrPath?.trim();
|
|
@@ -368,6 +574,9 @@ export function showKnowledge(projectRoot, idOrPath) {
|
|
|
368
574
|
body: extractMarkdownBody(content)
|
|
369
575
|
};
|
|
370
576
|
}
|
|
577
|
+
function sameKnowledgeSourceReference(left, right) {
|
|
578
|
+
return left.replace(/\\/g, "/").toLowerCase() === right.replace(/\\/g, "/").toLowerCase();
|
|
579
|
+
}
|
|
371
580
|
export function checkKnowledge(projectRoot) {
|
|
372
581
|
const knowledgeRoot = harnessPath(projectRoot, "knowledge");
|
|
373
582
|
const relativeRoot = path.relative(projectRoot, knowledgeRoot).split(path.sep).join(path.posix.sep);
|
|
@@ -431,6 +640,15 @@ export function checkKnowledge(projectRoot) {
|
|
|
431
640
|
if (contentValue && normalize(contentValue) === normalize(parsed.metadata.summary ?? "")) {
|
|
432
641
|
warnings.push(`${relativePath} summary duplicates the primary atomic content; use summary for context and the primary field for the durable rule.`);
|
|
433
642
|
}
|
|
643
|
+
if (parsed.metadata.status === "active" && parsed.metadata.sourcePaths.some((sourcePath) => sourcePath.toLowerCase().startsWith("ingest://"))) {
|
|
644
|
+
errors.push(...ingestLinkedAtomicMetadataErrors(relativePath, parsed.metadata));
|
|
645
|
+
}
|
|
646
|
+
for (const coverage of parsed.metadata.coverage.filter(isDraftPlaceholderCoverage)) {
|
|
647
|
+
errors.push(`${relativePath} coverage contains an unedited knowledge draft placeholder: ${coverage}.`);
|
|
648
|
+
}
|
|
649
|
+
for (const verification of parsed.metadata.verification.filter(isDraftPlaceholderVerification)) {
|
|
650
|
+
errors.push(`${relativePath} verification contains an unedited knowledge draft placeholder: ${verification}.`);
|
|
651
|
+
}
|
|
434
652
|
if (parsed.metadata.sourcePaths.some(isPrdSourcePath) && parsed.metadata.coverage.length === 0) {
|
|
435
653
|
warnings.push(`${relativePath} should include coverage items for PRD block traceability.`);
|
|
436
654
|
}
|
|
@@ -442,7 +660,7 @@ export function checkKnowledge(projectRoot) {
|
|
|
442
660
|
warnings.push(`${relativePath} frontmatter coverage disagrees with body traceability.`);
|
|
443
661
|
}
|
|
444
662
|
for (const sourcePath of parsed.metadata.sourcePaths) {
|
|
445
|
-
if (sourcePath && !isExternalReference(sourcePath) && !
|
|
663
|
+
if (sourcePath && !isExternalReference(sourcePath) && !isResolvedKnowledgeSource(projectRoot, sourcePath)) {
|
|
446
664
|
warnings.push(`${relativePath} source path is missing or unsafe: ${sourcePath}.`);
|
|
447
665
|
}
|
|
448
666
|
}
|
|
@@ -546,17 +764,22 @@ function resolveKnowledgeCardReference(cards, target) {
|
|
|
546
764
|
return idMatches[0] ?? null;
|
|
547
765
|
}
|
|
548
766
|
if (idMatches.length > 1) {
|
|
549
|
-
throw new Error(
|
|
767
|
+
throw new Error(formatAmbiguousKnowledgeReference(target, idMatches));
|
|
550
768
|
}
|
|
551
769
|
const pathSuffixMatches = cards.filter((item) => item.path.endsWith(`/${target}`));
|
|
552
770
|
if (pathSuffixMatches.length === 1) {
|
|
553
771
|
return pathSuffixMatches[0] ?? null;
|
|
554
772
|
}
|
|
555
773
|
if (pathSuffixMatches.length > 1) {
|
|
556
|
-
throw new Error(
|
|
774
|
+
throw new Error(formatAmbiguousKnowledgeReference(target, pathSuffixMatches));
|
|
557
775
|
}
|
|
558
776
|
return null;
|
|
559
777
|
}
|
|
778
|
+
function formatAmbiguousKnowledgeReference(target, matches) {
|
|
779
|
+
const selected = matches.slice(0, MAX_KNOWLEDGE_REFERENCE_SUGGESTIONS).map((item) => item.path);
|
|
780
|
+
const omitted = Math.max(0, matches.length - selected.length);
|
|
781
|
+
return `knowledge card reference is ambiguous: ${target}. Use --path with one of (selected: ${selected.length}; total: ${matches.length}; omitted: ${omitted}): ${selected.join(", ")}.`;
|
|
782
|
+
}
|
|
560
783
|
function parseKnowledgeFrontmatter(content) {
|
|
561
784
|
const metadata = {
|
|
562
785
|
scope: [],
|
|
@@ -932,6 +1155,9 @@ function validateKnowledgeSourcePath(value) {
|
|
|
932
1155
|
if (!value.trim()) {
|
|
933
1156
|
return "empty";
|
|
934
1157
|
}
|
|
1158
|
+
if (value.toLowerCase().startsWith("ingest://")) {
|
|
1159
|
+
return isIngestReference(value) ? null : "unsafe";
|
|
1160
|
+
}
|
|
935
1161
|
if (isExternalReference(value)) {
|
|
936
1162
|
return null;
|
|
937
1163
|
}
|
|
@@ -946,6 +1172,13 @@ function validateKnowledgeSourcePath(value) {
|
|
|
946
1172
|
function isKnowledgeSourceFile(projectRoot, sourcePath) {
|
|
947
1173
|
return isRegularFileWithoutSymlink(path.join(projectRoot, sourcePath));
|
|
948
1174
|
}
|
|
1175
|
+
function isResolvedKnowledgeSource(projectRoot, sourcePath) {
|
|
1176
|
+
if (isIngestReference(sourcePath)) {
|
|
1177
|
+
const parsed = parseIngestReference(sourcePath);
|
|
1178
|
+
return parsed !== null && isRegularFileWithoutSymlink(harnessPath(projectRoot, "ingest", parsed.kind, `${parsed.id}.json`));
|
|
1179
|
+
}
|
|
1180
|
+
return isKnowledgeSourceFile(projectRoot, sourcePath);
|
|
1181
|
+
}
|
|
949
1182
|
function mergeList(existing, next) {
|
|
950
1183
|
if (next === undefined) {
|
|
951
1184
|
return existing;
|
|
@@ -1142,11 +1375,29 @@ function extractMarkdownBody(content) {
|
|
|
1142
1375
|
return content.slice(endIndex).replace(/^\r?\n---\r?\n?/, "").trim();
|
|
1143
1376
|
}
|
|
1144
1377
|
function isExternalReference(value) {
|
|
1378
|
+
if (value.toLowerCase().startsWith("ingest://")) {
|
|
1379
|
+
return false;
|
|
1380
|
+
}
|
|
1145
1381
|
if (/^[A-Za-z]:/.test(value)) {
|
|
1146
1382
|
return false;
|
|
1147
1383
|
}
|
|
1148
1384
|
return /^[a-z][a-z0-9+.-]*:/i.test(value);
|
|
1149
1385
|
}
|
|
1386
|
+
function isIngestReference(value) {
|
|
1387
|
+
return /^ingest:\/\/(?:prd|openapi|ui-package)\/[a-z0-9][a-z0-9._-]{1,80}$/i.test(value)
|
|
1388
|
+
&& !value.includes("..")
|
|
1389
|
+
&& !/[?#]/.test(value);
|
|
1390
|
+
}
|
|
1391
|
+
function parseIngestReference(value) {
|
|
1392
|
+
const match = /^ingest:\/\/(prd|openapi|ui-package)\/([a-z0-9][a-z0-9._-]{1,80})$/i.exec(value);
|
|
1393
|
+
if (!match?.[1] || !match[2]) {
|
|
1394
|
+
return null;
|
|
1395
|
+
}
|
|
1396
|
+
return {
|
|
1397
|
+
kind: match[1].toLowerCase(),
|
|
1398
|
+
id: match[2]
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1150
1401
|
function isBroadCoverageItem(value) {
|
|
1151
1402
|
const normalized = value.trim().toLowerCase();
|
|
1152
1403
|
if (!normalized) {
|
|
@@ -1167,6 +1418,35 @@ function hasCoverageBodyDrift(projectRoot, card) {
|
|
|
1167
1418
|
const content = safeRead(path.join(projectRoot, card.path));
|
|
1168
1419
|
return content !== null && /\bCoverage:\s*Not specified\./i.test(extractMarkdownBody(content));
|
|
1169
1420
|
}
|
|
1421
|
+
function isDraftPlaceholderCoverage(value) {
|
|
1422
|
+
const normalized = value.trim().toLowerCase();
|
|
1423
|
+
return (normalized.startsWith("<") || normalized.endsWith(">"))
|
|
1424
|
+
&& (normalized.includes("precise section")
|
|
1425
|
+
|| normalized.includes("acceptance block")
|
|
1426
|
+
|| normalized === "<precise"
|
|
1427
|
+
|| normalized === "or acceptance block>");
|
|
1428
|
+
}
|
|
1429
|
+
function isDraftPlaceholderVerification(value) {
|
|
1430
|
+
const normalized = value.trim().toLowerCase();
|
|
1431
|
+
return /^<.*>$/.test(normalized)
|
|
1432
|
+
&& normalized.includes("specific")
|
|
1433
|
+
&& (normalized.includes("check") || normalized.includes("evidence"));
|
|
1434
|
+
}
|
|
1435
|
+
function ingestLinkedAtomicMetadataErrors(relativePath, metadata) {
|
|
1436
|
+
const errors = [];
|
|
1437
|
+
const requiredLists = [
|
|
1438
|
+
["scope", metadata.scope],
|
|
1439
|
+
["tags", metadata.tags],
|
|
1440
|
+
["coverage", metadata.coverage],
|
|
1441
|
+
["verification", metadata.verification]
|
|
1442
|
+
];
|
|
1443
|
+
for (const [field, values] of requiredLists) {
|
|
1444
|
+
if (values.length === 0) {
|
|
1445
|
+
errors.push(`${relativePath} active ingest-linked atomic knowledge must include ${field}.`);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return errors;
|
|
1449
|
+
}
|
|
1170
1450
|
function summarizePrdSources(projectRoot, cards, requiredSources = []) {
|
|
1171
1451
|
const grouped = new Map();
|
|
1172
1452
|
for (const sourcePath of discoverPrdSourceFiles(projectRoot)) {
|
|
@@ -1182,16 +1462,30 @@ function summarizePrdSources(projectRoot, cards, requiredSources = []) {
|
|
|
1182
1462
|
}
|
|
1183
1463
|
return [...grouped.entries()]
|
|
1184
1464
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
1185
|
-
.map(([sourcePath, sourceCards]) =>
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1465
|
+
.map(([sourcePath, sourceCards]) => {
|
|
1466
|
+
const cardIds = sourceCards.map((card) => card.id).sort();
|
|
1467
|
+
const scopes = [...new Set(sourceCards.flatMap((card) => card.scope))].sort();
|
|
1468
|
+
const tags = [...new Set(sourceCards.flatMap((card) => card.tags))].sort();
|
|
1469
|
+
const coverage = [...new Set(sourceCards.flatMap((card) => card.coverage))].sort();
|
|
1470
|
+
const prdAnchors = [...new Set(sourceCards.flatMap((card) => card.prdAnchors))].sort();
|
|
1471
|
+
return {
|
|
1472
|
+
path: sourcePath,
|
|
1473
|
+
cardCount: sourceCards.length,
|
|
1474
|
+
activeCardCount: sourceCards.filter((card) => card.status === "active").length,
|
|
1475
|
+
cardIds: selectedIndexDetails(cardIds),
|
|
1476
|
+
scopes: selectedIndexDetails(scopes),
|
|
1477
|
+
tags: selectedIndexDetails(tags),
|
|
1478
|
+
coverage: selectedIndexDetails(coverage),
|
|
1479
|
+
prdAnchors: selectedIndexDetails(prdAnchors),
|
|
1480
|
+
omitted: {
|
|
1481
|
+
cardIds: omittedIndexDetails(cardIds),
|
|
1482
|
+
scopes: omittedIndexDetails(scopes),
|
|
1483
|
+
tags: omittedIndexDetails(tags),
|
|
1484
|
+
coverage: omittedIndexDetails(coverage),
|
|
1485
|
+
prdAnchors: omittedIndexDetails(prdAnchors)
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1488
|
+
});
|
|
1195
1489
|
}
|
|
1196
1490
|
function summarizeModules(cards) {
|
|
1197
1491
|
const atomicCards = cards.filter((card) => card.kind && card.status === "active");
|
|
@@ -1204,6 +1498,7 @@ function summarizeModules(cards) {
|
|
|
1204
1498
|
card.related.includes(moduleCard.id) ||
|
|
1205
1499
|
card.sourcePaths.some((sourcePath) => moduleSources.has(sourcePath)) ||
|
|
1206
1500
|
card.scope.some((scope) => moduleCard.scope.includes(scope)));
|
|
1501
|
+
const atomicCardIds = relatedAtomicCards.map((card) => card.id).sort();
|
|
1207
1502
|
return {
|
|
1208
1503
|
id: moduleCard.id,
|
|
1209
1504
|
path: moduleCard.path,
|
|
@@ -1214,11 +1509,18 @@ function summarizeModules(cards) {
|
|
|
1214
1509
|
tags: moduleCard.tags,
|
|
1215
1510
|
related: moduleCard.related,
|
|
1216
1511
|
atomicCardCount: relatedAtomicCards.length,
|
|
1217
|
-
atomicCardIds:
|
|
1512
|
+
atomicCardIds: selectedIndexDetails(atomicCardIds),
|
|
1513
|
+
omittedAtomicCardIds: omittedIndexDetails(atomicCardIds)
|
|
1218
1514
|
};
|
|
1219
1515
|
})
|
|
1220
1516
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
1221
1517
|
}
|
|
1518
|
+
function selectedIndexDetails(values) {
|
|
1519
|
+
return values.slice(0, MAX_KNOWLEDGE_INDEX_DETAIL_ITEMS);
|
|
1520
|
+
}
|
|
1521
|
+
function omittedIndexDetails(values) {
|
|
1522
|
+
return Math.max(0, values.length - MAX_KNOWLEDGE_INDEX_DETAIL_ITEMS);
|
|
1523
|
+
}
|
|
1222
1524
|
function discoverPrdSourceFiles(projectRoot) {
|
|
1223
1525
|
const roots = ["docs/prd", "docs/requirements", "prd", "requirements"];
|
|
1224
1526
|
const rootSources = roots.flatMap((root) => {
|
|
@@ -1237,6 +1539,9 @@ function discoverPrdSourceFiles(projectRoot) {
|
|
|
1237
1539
|
}
|
|
1238
1540
|
function isPrdSourcePath(sourcePath) {
|
|
1239
1541
|
const normalized = sourcePath.replace(/\\/g, "/");
|
|
1542
|
+
if (/^ingest:\/\/prd\//i.test(normalized)) {
|
|
1543
|
+
return isIngestReference(normalized);
|
|
1544
|
+
}
|
|
1240
1545
|
if (/(^|\/)(prd|requirements?)(\/|[-_.])|(^|\/)docs\/(prd|requirements?)(\/|$)/i.test(normalized)) {
|
|
1241
1546
|
return true;
|
|
1242
1547
|
}
|