its-magic 0.1.2-37 → 0.1.2-39
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/installer.ps1 +20 -0
- package/installer.py +66 -2
- package/installer.sh +22 -0
- package/package.json +2 -1
- package/scripts/check_intake_template_parity.py +1 -0
- package/scripts/intake_evidence_lib.py +413 -10
- package/scripts/intake_evidence_validate.py +2 -2
- package/scripts/materialize_codebase_map.py +184 -0
- package/template/.cursor/agents/po.mdc +19 -0
- package/template/.cursor/commands/architecture.md +12 -0
- package/template/.cursor/commands/ask.md +11 -0
- package/template/.cursor/commands/auto.md +20 -2
- package/template/.cursor/commands/intake.md +64 -9
- package/template/.cursor/commands/map-codebase.md +18 -1
- package/template/.cursor/commands/refresh-context.md +7 -0
- package/template/.cursor/rules/core.mdc +5 -0
- package/template/docs/engineering/artifact-ownership-policy.md +1 -1
- package/template/docs/engineering/context/installer-owned-paths.manifest +17 -0
- package/template/docs/engineering/runbook.md +76 -2
- package/template/scripts/check_intake_template_parity.py +1 -0
- package/template/scripts/enforce-triad-hot-surface.py +626 -0
- package/template/scripts/intake_bug_resume_brief_refresh.py +303 -0
- package/template/scripts/intake_evidence_lib.py +413 -10
- package/template/scripts/intake_evidence_validate.py +2 -2
- package/template/scripts/materialize_codebase_map.py +184 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Deterministic intake evidence validation
|
|
2
|
+
Deterministic intake evidence validation
|
|
3
|
+
(US-0078 / US-0083 / DEC-0060 / DEC-0067 / R-0055 / BUG-0007 / R-0066).
|
|
3
4
|
|
|
4
5
|
Consumes a logical intake_evidence bundle (dict). PO workflows MUST run this
|
|
5
6
|
gate before mutating backlog/acceptance; failures are fail-closed.
|
|
@@ -14,6 +15,7 @@ from dataclasses import dataclass, field
|
|
|
14
15
|
from typing import Any
|
|
15
16
|
|
|
16
17
|
IE_REF_RE = re.compile(r"^ie:([^:]+):(\d+):([0-9a-f]{16})$")
|
|
18
|
+
PLAN_AREA_ID_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{1,63}$")
|
|
17
19
|
|
|
18
20
|
PACK_REQUIRED_KEYS: dict[str, tuple[str, ...]] = {
|
|
19
21
|
"first-intake-pack": (
|
|
@@ -51,6 +53,8 @@ SAFE_ASSUMPTION_LITERALS = frozenset(
|
|
|
51
53
|
)
|
|
52
54
|
|
|
53
55
|
FALSE_CONFIRMATION_LITERALS = frozenset({"yes", "true", "confirmed"})
|
|
56
|
+
ALLOWED_SATISFIED_BY = frozenset({"answer_ref", "assumption_confirmation_ref", "delegation_ref"})
|
|
57
|
+
DELEGATION_CONFIDENCE_VALUES = frozenset({"low", "medium", "high"})
|
|
54
58
|
|
|
55
59
|
|
|
56
60
|
def canonical_json_sha256_16(obj: dict[str, Any]) -> str:
|
|
@@ -187,6 +191,268 @@ def _row_turn(row: dict[str, Any]) -> int | None:
|
|
|
187
191
|
return None
|
|
188
192
|
|
|
189
193
|
|
|
194
|
+
def _row_uses_equivalent_evidence(row: dict[str, Any]) -> bool:
|
|
195
|
+
"""
|
|
196
|
+
US-0083 AC-1: allow required-topic accounting without forcing repetitive asks
|
|
197
|
+
when equivalent evidence is already captured and explicitly referenced.
|
|
198
|
+
"""
|
|
199
|
+
marker = str(row.get("evidence_source") or "").strip()
|
|
200
|
+
if marker != "equivalent_evidence_ref":
|
|
201
|
+
return False
|
|
202
|
+
return bool(str(row.get("equivalent_evidence_ref") or "").strip())
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _norm_answer_ref_quoted_user_text(value: Any) -> str:
|
|
206
|
+
"""Normalize quoted_user_text for BUG-0007 duplicate detection (DEC-0060 strip parity)."""
|
|
207
|
+
if value is None:
|
|
208
|
+
return ""
|
|
209
|
+
return str(value).strip()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _row_exempt_from_answer_ref_topic_distinctness(row: dict[str, Any]) -> bool:
|
|
213
|
+
"""BUG-0007: alternate satisfaction paths do not participate in answer_ref blob reuse checks."""
|
|
214
|
+
return _row_uses_equivalent_evidence(row)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _validate_answer_ref_topic_distinctness(
|
|
218
|
+
bundle: dict[str, Any],
|
|
219
|
+
required: list[str],
|
|
220
|
+
by_key: dict[str, dict[str, Any]],
|
|
221
|
+
res: ValidationResult,
|
|
222
|
+
) -> None:
|
|
223
|
+
"""
|
|
224
|
+
BUG-0007 / R-0066: distinct required topic_key rows must not reuse the same
|
|
225
|
+
quoted_user_text under satisfied_by=answer_ref (normalized), except exempt rows.
|
|
226
|
+
"""
|
|
227
|
+
norm_to_topics: dict[str, list[str]] = {}
|
|
228
|
+
for k in required:
|
|
229
|
+
row = by_key.get(k)
|
|
230
|
+
if not row:
|
|
231
|
+
continue
|
|
232
|
+
sat = (row.get("satisfied_by") or "").strip()
|
|
233
|
+
if sat != "answer_ref":
|
|
234
|
+
continue
|
|
235
|
+
if _row_exempt_from_answer_ref_topic_distinctness(row):
|
|
236
|
+
continue
|
|
237
|
+
irid = _row_run_id(bundle, row)
|
|
238
|
+
tit = _row_turn(row)
|
|
239
|
+
if irid is None or tit is None:
|
|
240
|
+
continue
|
|
241
|
+
qraw = row.get("quoted_user_text")
|
|
242
|
+
qtxt = "" if qraw is None else str(qraw)
|
|
243
|
+
ref = (row.get("ref") or "").strip()
|
|
244
|
+
if not ref or not verify_ie_ref(
|
|
245
|
+
ref,
|
|
246
|
+
intake_run_id=irid,
|
|
247
|
+
turn_index=int(tit),
|
|
248
|
+
topic_key=k,
|
|
249
|
+
satisfied_by=sat,
|
|
250
|
+
quoted_user_text=qtxt,
|
|
251
|
+
):
|
|
252
|
+
continue
|
|
253
|
+
norm = _norm_answer_ref_quoted_user_text(qraw)
|
|
254
|
+
norm_to_topics.setdefault(norm, []).append(k)
|
|
255
|
+
|
|
256
|
+
dup_groups: list[str] = []
|
|
257
|
+
for norm, topics in norm_to_topics.items():
|
|
258
|
+
uniq = sorted(set(topics))
|
|
259
|
+
if len(uniq) < 2:
|
|
260
|
+
continue
|
|
261
|
+
dup_groups.append("text=" + repr(norm[:120]) + " topics=" + ",".join(uniq))
|
|
262
|
+
|
|
263
|
+
if dup_groups:
|
|
264
|
+
res.ok = False
|
|
265
|
+
res.add_code("INTAKE_ANSWER_REF_NOT_TOPIC_DISTINCT")
|
|
266
|
+
res.diagnostics.append(
|
|
267
|
+
"Remediation: distinct required topics must not reuse the same quoted_user_text "
|
|
268
|
+
"under satisfied_by=answer_ref (BUG-0007 / R-0066). Duplicates: "
|
|
269
|
+
+ "; ".join(dup_groups)
|
|
270
|
+
+ ". Use per-topic answers, or an allowed alternate path "
|
|
271
|
+
"(evidence_source=equivalent_evidence_ref + equivalent_evidence_ref, "
|
|
272
|
+
"delegation_ref per DEC-0067 / US-0083, or assumption_confirmation_ref on the row)."
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _candidate_story_ids(bundle: dict[str, Any]) -> set[str]:
|
|
277
|
+
out: set[str] = set()
|
|
278
|
+
|
|
279
|
+
raw_ids = bundle.get("candidate_story_ids")
|
|
280
|
+
if isinstance(raw_ids, list):
|
|
281
|
+
for sid in raw_ids:
|
|
282
|
+
sv = str(sid).strip()
|
|
283
|
+
if sv:
|
|
284
|
+
out.add(sv)
|
|
285
|
+
|
|
286
|
+
raw_story_ids = bundle.get("story_ids")
|
|
287
|
+
if isinstance(raw_story_ids, list):
|
|
288
|
+
for sid in raw_story_ids:
|
|
289
|
+
sv = str(sid).strip()
|
|
290
|
+
if sv:
|
|
291
|
+
out.add(sv)
|
|
292
|
+
|
|
293
|
+
raw_story_map = bundle.get("story_map")
|
|
294
|
+
if isinstance(raw_story_map, list):
|
|
295
|
+
for row in raw_story_map:
|
|
296
|
+
if not isinstance(row, dict):
|
|
297
|
+
continue
|
|
298
|
+
sv = str(row.get("story_id") or "").strip()
|
|
299
|
+
if sv:
|
|
300
|
+
out.add(sv)
|
|
301
|
+
|
|
302
|
+
return out
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _validate_plan_coverage_contract(bundle: dict[str, Any], res: ValidationResult) -> None:
|
|
306
|
+
inventory_raw = bundle.get("plan_area_inventory")
|
|
307
|
+
coverage_raw = bundle.get("plan_area_coverage")
|
|
308
|
+
coverage_complete_raw = bundle.get("coverage_complete")
|
|
309
|
+
candidate_story_ids = _candidate_story_ids(bundle)
|
|
310
|
+
|
|
311
|
+
inventory_rows = inventory_raw if isinstance(inventory_raw, list) else []
|
|
312
|
+
coverage_rows = coverage_raw if isinstance(coverage_raw, list) else []
|
|
313
|
+
|
|
314
|
+
inventory_ids: list[str] = []
|
|
315
|
+
coverage_by_id: dict[str, dict[str, Any]] = {}
|
|
316
|
+
|
|
317
|
+
coverage_missing = False
|
|
318
|
+
id_invalid = False
|
|
319
|
+
contract_invalid = False
|
|
320
|
+
deferred_ref_missing = False
|
|
321
|
+
|
|
322
|
+
if not inventory_rows:
|
|
323
|
+
coverage_missing = True
|
|
324
|
+
res.diagnostics.append(
|
|
325
|
+
"Remediation: plan_area_inventory must be a non-empty list for first/new/broad intake."
|
|
326
|
+
)
|
|
327
|
+
if not coverage_rows:
|
|
328
|
+
coverage_missing = True
|
|
329
|
+
res.diagnostics.append(
|
|
330
|
+
"Remediation: plan_area_coverage must be a non-empty list for first/new/broad intake."
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
seen_inventory_ids: set[str] = set()
|
|
334
|
+
for row in inventory_rows:
|
|
335
|
+
if not isinstance(row, dict):
|
|
336
|
+
id_invalid = True
|
|
337
|
+
continue
|
|
338
|
+
plan_area_id = str(row.get("plan_area_id") or "").strip()
|
|
339
|
+
if not PLAN_AREA_ID_RE.match(plan_area_id):
|
|
340
|
+
id_invalid = True
|
|
341
|
+
continue
|
|
342
|
+
if plan_area_id in seen_inventory_ids:
|
|
343
|
+
id_invalid = True
|
|
344
|
+
continue
|
|
345
|
+
seen_inventory_ids.add(plan_area_id)
|
|
346
|
+
inventory_ids.append(plan_area_id)
|
|
347
|
+
|
|
348
|
+
for row in coverage_rows:
|
|
349
|
+
if not isinstance(row, dict):
|
|
350
|
+
id_invalid = True
|
|
351
|
+
continue
|
|
352
|
+
plan_area_id = str(row.get("plan_area_id") or "").strip()
|
|
353
|
+
if not PLAN_AREA_ID_RE.match(plan_area_id):
|
|
354
|
+
id_invalid = True
|
|
355
|
+
continue
|
|
356
|
+
if plan_area_id in coverage_by_id:
|
|
357
|
+
id_invalid = True
|
|
358
|
+
continue
|
|
359
|
+
coverage_by_id[plan_area_id] = row
|
|
360
|
+
|
|
361
|
+
inventory_id_set = set(inventory_ids)
|
|
362
|
+
coverage_id_set = set(coverage_by_id.keys())
|
|
363
|
+
|
|
364
|
+
if inventory_id_set != coverage_id_set:
|
|
365
|
+
coverage_missing = True
|
|
366
|
+
missing_ids = sorted(inventory_id_set - coverage_id_set)
|
|
367
|
+
extra_ids = sorted(coverage_id_set - inventory_id_set)
|
|
368
|
+
if missing_ids:
|
|
369
|
+
res.diagnostics.append(
|
|
370
|
+
"Remediation: add plan_area_coverage rows for uncovered plan_area_id values: "
|
|
371
|
+
+ ", ".join(missing_ids)
|
|
372
|
+
)
|
|
373
|
+
if extra_ids:
|
|
374
|
+
contract_invalid = True
|
|
375
|
+
res.diagnostics.append(
|
|
376
|
+
"Remediation: remove unknown plan_area_coverage plan_area_id values not in plan_area_inventory: "
|
|
377
|
+
+ ", ".join(extra_ids)
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
for plan_area_id in sorted(inventory_id_set & coverage_id_set):
|
|
381
|
+
row = coverage_by_id[plan_area_id]
|
|
382
|
+
story_ids_raw = row.get("story_ids")
|
|
383
|
+
deferred_ref = str(row.get("deferred_ref") or "").strip()
|
|
384
|
+
|
|
385
|
+
story_ids: list[str] = []
|
|
386
|
+
if isinstance(story_ids_raw, list):
|
|
387
|
+
for sid in story_ids_raw:
|
|
388
|
+
sv = str(sid).strip()
|
|
389
|
+
if sv:
|
|
390
|
+
story_ids.append(sv)
|
|
391
|
+
has_story_ids = bool(story_ids)
|
|
392
|
+
has_deferred_ref = bool(deferred_ref)
|
|
393
|
+
|
|
394
|
+
if has_story_ids == has_deferred_ref:
|
|
395
|
+
contract_invalid = True
|
|
396
|
+
res.diagnostics.append(
|
|
397
|
+
"Remediation: each plan_area_coverage row must set exactly one mapping path "
|
|
398
|
+
"(story_ids xor deferred_ref) for plan_area_id "
|
|
399
|
+
+ repr(plan_area_id)
|
|
400
|
+
+ "."
|
|
401
|
+
)
|
|
402
|
+
continue
|
|
403
|
+
|
|
404
|
+
if has_story_ids:
|
|
405
|
+
if candidate_story_ids:
|
|
406
|
+
unknown_story_ids = sorted({sid for sid in story_ids if sid not in candidate_story_ids})
|
|
407
|
+
if unknown_story_ids:
|
|
408
|
+
contract_invalid = True
|
|
409
|
+
res.diagnostics.append(
|
|
410
|
+
"Remediation: plan_area_id "
|
|
411
|
+
+ repr(plan_area_id)
|
|
412
|
+
+ " references unknown story_ids not present in candidate story set: "
|
|
413
|
+
+ ", ".join(unknown_story_ids)
|
|
414
|
+
)
|
|
415
|
+
else:
|
|
416
|
+
deferred_reason = str(row.get("deferred_reason") or "").strip()
|
|
417
|
+
if not deferred_reason:
|
|
418
|
+
deferred_ref_missing = True
|
|
419
|
+
res.diagnostics.append(
|
|
420
|
+
"Remediation: deferred mapping for plan_area_id "
|
|
421
|
+
+ repr(plan_area_id)
|
|
422
|
+
+ " requires both deferred_ref and deferred_reason."
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
derived_coverage_complete = not (coverage_missing or id_invalid or contract_invalid or deferred_ref_missing)
|
|
426
|
+
if coverage_complete_raw is not True:
|
|
427
|
+
contract_invalid = True
|
|
428
|
+
res.diagnostics.append(
|
|
429
|
+
"Remediation: set coverage_complete=true only after plan_area_inventory and "
|
|
430
|
+
"plan_area_coverage pass deterministic validation."
|
|
431
|
+
)
|
|
432
|
+
if bool(coverage_complete_raw) != derived_coverage_complete:
|
|
433
|
+
contract_invalid = True
|
|
434
|
+
res.diagnostics.append(
|
|
435
|
+
"Remediation: coverage_complete must match derived contract result from "
|
|
436
|
+
"plan_area_inventory/plan_area_coverage validation."
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
if coverage_missing:
|
|
440
|
+
res.ok = False
|
|
441
|
+
res.add_code("INTAKE_PLAN_COVERAGE_MISSING")
|
|
442
|
+
if id_invalid:
|
|
443
|
+
res.ok = False
|
|
444
|
+
res.add_code("INTAKE_PLAN_AREA_ID_INVALID")
|
|
445
|
+
res.diagnostics.append(
|
|
446
|
+
"Remediation: plan_area_id must be unique and match ^[a-z0-9][a-z0-9_-]{1,63}$ in inventory and coverage."
|
|
447
|
+
)
|
|
448
|
+
if deferred_ref_missing:
|
|
449
|
+
res.ok = False
|
|
450
|
+
res.add_code("INTAKE_PLAN_DEFERRED_REF_MISSING")
|
|
451
|
+
if contract_invalid:
|
|
452
|
+
res.ok = False
|
|
453
|
+
res.add_code("INTAKE_PLAN_COVERAGE_CONTRACT_INVALID")
|
|
454
|
+
|
|
455
|
+
|
|
190
456
|
def validate_intake_evidence(
|
|
191
457
|
bundle: dict[str, Any],
|
|
192
458
|
*,
|
|
@@ -232,15 +498,44 @@ def validate_intake_evidence(
|
|
|
232
498
|
qtxt = ""
|
|
233
499
|
qtxt = str(qtxt)
|
|
234
500
|
|
|
235
|
-
if sat not in
|
|
501
|
+
if sat not in ALLOWED_SATISFIED_BY:
|
|
236
502
|
res.ok = False
|
|
237
503
|
res.diagnostics.append(
|
|
238
504
|
f"Remediation: topic {k!r} must set satisfied_by to "
|
|
239
|
-
f"'answer_ref' or '
|
|
505
|
+
f"'answer_ref', 'assumption_confirmation_ref', or 'delegation_ref' (got {sat!r})."
|
|
240
506
|
)
|
|
241
507
|
missing_cov.append(k)
|
|
242
508
|
continue
|
|
243
509
|
|
|
510
|
+
if sat == "delegation_ref":
|
|
511
|
+
scope = str(row.get("delegation_scope") or "").strip()
|
|
512
|
+
rationale = str(row.get("delegation_rationale") or "").strip()
|
|
513
|
+
confidence = str(row.get("delegation_confidence") or "").strip().lower()
|
|
514
|
+
if not scope or not rationale or not confidence:
|
|
515
|
+
res.ok = False
|
|
516
|
+
res.add_code("INTAKE_DELEGATION_EVIDENCE_MISSING")
|
|
517
|
+
missing_fields = []
|
|
518
|
+
if not scope:
|
|
519
|
+
missing_fields.append("delegation_scope")
|
|
520
|
+
if not rationale:
|
|
521
|
+
missing_fields.append("delegation_rationale")
|
|
522
|
+
if not confidence:
|
|
523
|
+
missing_fields.append("delegation_confidence")
|
|
524
|
+
res.diagnostics.append(
|
|
525
|
+
f"Remediation: delegated topic {k!r} requires non-empty fields: "
|
|
526
|
+
+ ", ".join(missing_fields)
|
|
527
|
+
+ "."
|
|
528
|
+
)
|
|
529
|
+
continue
|
|
530
|
+
if confidence not in DELEGATION_CONFIDENCE_VALUES:
|
|
531
|
+
res.ok = False
|
|
532
|
+
res.add_code("INTAKE_DELEGATION_EVIDENCE_INVALID")
|
|
533
|
+
res.diagnostics.append(
|
|
534
|
+
f"Remediation: delegated topic {k!r} delegation_confidence must be one of "
|
|
535
|
+
f"{sorted(DELEGATION_CONFIDENCE_VALUES)!r} (got {confidence!r})."
|
|
536
|
+
)
|
|
537
|
+
continue
|
|
538
|
+
|
|
244
539
|
irid = _row_run_id(bundle, row)
|
|
245
540
|
tit = _row_turn(row)
|
|
246
541
|
if irid is None or tit is None:
|
|
@@ -261,11 +556,18 @@ def validate_intake_evidence(
|
|
|
261
556
|
quoted_user_text=qtxt,
|
|
262
557
|
):
|
|
263
558
|
res.ok = False
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
559
|
+
if sat == "delegation_ref":
|
|
560
|
+
res.add_code("INTAKE_DELEGATION_EVIDENCE_INVALID")
|
|
561
|
+
res.diagnostics.append(
|
|
562
|
+
f"Remediation: delegated topic {k!r} ref is malformed or hash mismatch — rebuild ie: ref "
|
|
563
|
+
f"with DEC-0060 canonical JSON (sorted keys) and quoted_user_text."
|
|
564
|
+
)
|
|
565
|
+
else:
|
|
566
|
+
res.diagnostics.append(
|
|
567
|
+
f"Remediation: topic {k!r} ref is malformed or hash mismatch — rebuild ie: ref "
|
|
568
|
+
f"with DEC-0060 canonical JSON (sorted keys) and quoted_user_text."
|
|
569
|
+
)
|
|
570
|
+
missing_cov.append(k)
|
|
269
571
|
|
|
270
572
|
if missing_cov:
|
|
271
573
|
res.ok = False
|
|
@@ -283,16 +585,25 @@ def validate_intake_evidence(
|
|
|
283
585
|
if k not in by_key:
|
|
284
586
|
continue
|
|
285
587
|
if k not in asked:
|
|
588
|
+
row = by_key[k]
|
|
589
|
+
if _row_uses_equivalent_evidence(row):
|
|
590
|
+
continue
|
|
286
591
|
res.ok = False
|
|
287
592
|
res.add_code("INTAKE_REQUIRED_TOPIC_MISSING")
|
|
288
593
|
res.diagnostics.append(
|
|
289
|
-
f"Remediation: add {k!r} to asked_topics
|
|
290
|
-
f"
|
|
594
|
+
f"Remediation: add {k!r} to asked_topics or mark evidence_source='equivalent_evidence_ref' "
|
|
595
|
+
f"with equivalent_evidence_ref when reusing previously captured equivalent evidence."
|
|
291
596
|
)
|
|
292
597
|
if k not in res.missing_topics:
|
|
293
598
|
res.missing_topics.append(k)
|
|
294
599
|
res.missing_topics = sorted(set(res.missing_topics))
|
|
295
600
|
|
|
601
|
+
_validate_answer_ref_topic_distinctness(bundle, required, by_key, res)
|
|
602
|
+
|
|
603
|
+
# US-0081 / DEC-0064: first/new/broad intake requires complete-plan coverage contract.
|
|
604
|
+
if pack == "first-intake-pack":
|
|
605
|
+
_validate_plan_coverage_contract(bundle, res)
|
|
606
|
+
|
|
296
607
|
ac = bundle.get("assumptions_confirmed")
|
|
297
608
|
ac_str = ac if isinstance(ac, str) else ("(none)" if ac is None else str(ac))
|
|
298
609
|
|
|
@@ -397,3 +708,95 @@ def self_test() -> None:
|
|
|
397
708
|
r1 = validate_intake_evidence(bundle, intake_guided_mode=1)
|
|
398
709
|
assert r0.ok and r1.ok
|
|
399
710
|
assert r0.primary_codes == r1.primary_codes
|
|
711
|
+
|
|
712
|
+
# US-0083 delegated-topic pass path
|
|
713
|
+
delegated_key = "done_definition"
|
|
714
|
+
rows2 = []
|
|
715
|
+
for i, key in enumerate(small):
|
|
716
|
+
sat = "delegation_ref" if key == delegated_key else "answer_ref"
|
|
717
|
+
txt = f"d{i}"
|
|
718
|
+
row = {
|
|
719
|
+
"topic_key": key,
|
|
720
|
+
"satisfied_by": sat,
|
|
721
|
+
"quoted_user_text": txt,
|
|
722
|
+
"intake_run_id": rid,
|
|
723
|
+
"turn_index": 200 + i,
|
|
724
|
+
"ref": build_ie_ref(rid, 200 + i, key, sat, txt),
|
|
725
|
+
}
|
|
726
|
+
if sat == "delegation_ref":
|
|
727
|
+
row["delegation_scope"] = "Implementation defaults for done criteria wording"
|
|
728
|
+
row["delegation_rationale"] = "User asked to proceed without additional specificity."
|
|
729
|
+
row["delegation_confidence"] = "medium"
|
|
730
|
+
rows2.append(row)
|
|
731
|
+
delegated_bundle = {
|
|
732
|
+
"selected_pack": "small-intake-pack",
|
|
733
|
+
"intake_run_id": rid,
|
|
734
|
+
"asked_topics": list(small),
|
|
735
|
+
"missing_topics": [],
|
|
736
|
+
"assumptions_confirmed": "(none)",
|
|
737
|
+
"topic_coverage": rows2,
|
|
738
|
+
}
|
|
739
|
+
d0 = validate_intake_evidence(delegated_bundle, intake_guided_mode=0)
|
|
740
|
+
d1 = validate_intake_evidence(delegated_bundle, intake_guided_mode=1)
|
|
741
|
+
assert d0.ok and d1.ok
|
|
742
|
+
assert d0.primary_codes == d1.primary_codes
|
|
743
|
+
|
|
744
|
+
# BUG-0007: same quoted_user_text across multiple answer_ref required topics fails
|
|
745
|
+
dup_txt = "synthetic blob echoed for every topic"
|
|
746
|
+
dup_rows = []
|
|
747
|
+
for i, key in enumerate(small):
|
|
748
|
+
dup_rows.append(
|
|
749
|
+
{
|
|
750
|
+
"topic_key": key,
|
|
751
|
+
"satisfied_by": "answer_ref",
|
|
752
|
+
"quoted_user_text": dup_txt,
|
|
753
|
+
"intake_run_id": rid,
|
|
754
|
+
"turn_index": 300 + i,
|
|
755
|
+
"ref": build_ie_ref(rid, 300 + i, key, "answer_ref", dup_txt),
|
|
756
|
+
}
|
|
757
|
+
)
|
|
758
|
+
dup_bundle = {
|
|
759
|
+
"selected_pack": "small-intake-pack",
|
|
760
|
+
"intake_run_id": rid,
|
|
761
|
+
"asked_topics": list(small),
|
|
762
|
+
"missing_topics": [],
|
|
763
|
+
"assumptions_confirmed": "(none)",
|
|
764
|
+
"topic_coverage": dup_rows,
|
|
765
|
+
}
|
|
766
|
+
dup_res = validate_intake_evidence(dup_bundle)
|
|
767
|
+
assert not dup_res.ok
|
|
768
|
+
assert "INTAKE_ANSWER_REF_NOT_TOPIC_DISTINCT" in dup_res.primary_codes
|
|
769
|
+
|
|
770
|
+
# First-intake full-plan coverage contract (US-0081 / DEC-0064)
|
|
771
|
+
first = PACK_REQUIRED_KEYS["first-intake-pack"]
|
|
772
|
+
first_rows = []
|
|
773
|
+
for i, key in enumerate(first):
|
|
774
|
+
first_rows.append(
|
|
775
|
+
{
|
|
776
|
+
"topic_key": key,
|
|
777
|
+
"satisfied_by": "answer_ref",
|
|
778
|
+
"quoted_user_text": f"b{i}",
|
|
779
|
+
"intake_run_id": rid,
|
|
780
|
+
"turn_index": i,
|
|
781
|
+
"ref": build_ie_ref(rid, i, key, "answer_ref", f"b{i}"),
|
|
782
|
+
}
|
|
783
|
+
)
|
|
784
|
+
full_bundle = {
|
|
785
|
+
"selected_pack": "first-intake-pack",
|
|
786
|
+
"intake_run_id": rid,
|
|
787
|
+
"asked_topics": list(first),
|
|
788
|
+
"missing_topics": [],
|
|
789
|
+
"assumptions_confirmed": "(none)",
|
|
790
|
+
"topic_coverage": first_rows,
|
|
791
|
+
"candidate_story_ids": ["US-9001", "US-9002"],
|
|
792
|
+
"plan_area_inventory": [
|
|
793
|
+
{"plan_area_id": "auth", "title": "Auth"},
|
|
794
|
+
{"plan_area_id": "billing", "title": "Billing"},
|
|
795
|
+
],
|
|
796
|
+
"plan_area_coverage": [
|
|
797
|
+
{"plan_area_id": "auth", "story_ids": ["US-9001"]},
|
|
798
|
+
{"plan_area_id": "billing", "story_ids": ["US-9002"]},
|
|
799
|
+
],
|
|
800
|
+
"coverage_complete": True,
|
|
801
|
+
}
|
|
802
|
+
assert validate_intake_evidence(full_bundle, intake_guided_mode=0).ok
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
Validate intake_evidence JSON bundles (US-0078 / DEC-0060).
|
|
3
|
+
Validate intake_evidence JSON bundles (US-0078 / US-0083 / DEC-0060 / DEC-0067).
|
|
4
4
|
|
|
5
5
|
Used by PO workflow preflight and CI fixtures.
|
|
6
6
|
"""
|
|
@@ -21,7 +21,7 @@ import intake_evidence_lib # noqa: E402
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def main() -> int:
|
|
24
|
-
p = argparse.ArgumentParser(description="Validate intake_evidence JSON (US-0078).")
|
|
24
|
+
p = argparse.ArgumentParser(description="Validate intake_evidence JSON (US-0078/US-0083).")
|
|
25
25
|
p.add_argument("--file", help="Path to JSON file containing one intake_evidence object.")
|
|
26
26
|
p.add_argument(
|
|
27
27
|
"--stdin",
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Idempotent codebase map bootstrap (US-0082 / DEC-0065).
|
|
3
|
+
|
|
4
|
+
Writes only docs/engineering/codebase-map.md and docs/engineering/dependencies.json
|
|
5
|
+
per /map-codebase contract. Does not append docs/engineering/state.md.
|
|
6
|
+
|
|
7
|
+
Diagnostics on stdout use deterministic tokens: [CODEBASE_MAP_OK] ... and CODEBASE_MAP_BLOCKED:...
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
BOOTSTRAP_SENTINEL = "<!-- its-magic:codebase-map-bootstrap v1 -->"
|
|
18
|
+
|
|
19
|
+
REMEDIATION = (
|
|
20
|
+
"Remediation: run `/map-codebase` for a full pass; see "
|
|
21
|
+
"`docs/engineering/runbook.md` (Codebase map bootstrap) and "
|
|
22
|
+
"`docs/engineering/architecture.md` (# US-0082)."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _bootstrap_map_markdown() -> str:
|
|
27
|
+
lines = [
|
|
28
|
+
"# Codebase Map",
|
|
29
|
+
"",
|
|
30
|
+
BOOTSTRAP_SENTINEL,
|
|
31
|
+
"",
|
|
32
|
+
"This is a **bootstrap** codebase map created by the lifecycle materializer "
|
|
33
|
+
"(**US-0082** / **DEC-0065**). It satisfies the “map exists” contract for fresh repos.",
|
|
34
|
+
"",
|
|
35
|
+
"For a full repository analysis, run **`/map-codebase`** (explicit/manual).",
|
|
36
|
+
"",
|
|
37
|
+
"## Stack",
|
|
38
|
+
"",
|
|
39
|
+
"| Aspect | Detail |",
|
|
40
|
+
"|--------|--------|",
|
|
41
|
+
"| (pending) | Run `/map-codebase` or edit this table after local analysis |",
|
|
42
|
+
"",
|
|
43
|
+
"## Entry Points",
|
|
44
|
+
"",
|
|
45
|
+
"| Entry | File | Purpose |",
|
|
46
|
+
"|-------|------|---------|",
|
|
47
|
+
"| (pending) | — | Run `/map-codebase` to populate |",
|
|
48
|
+
"",
|
|
49
|
+
"## Next steps",
|
|
50
|
+
"",
|
|
51
|
+
"1. Run **`/map-codebase`** for a deep pass, **or**",
|
|
52
|
+
"2. Edit this file directly with project-specific structure.",
|
|
53
|
+
"",
|
|
54
|
+
]
|
|
55
|
+
return "\n".join(lines)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _bootstrap_dependencies_obj() -> dict:
|
|
59
|
+
return {"libraries": [], "runtime": [], "tooling": []}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _bootstrap_dependencies_text() -> str:
|
|
63
|
+
return json.dumps(_bootstrap_dependencies_obj(), sort_keys=True, indent=2) + "\n"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _normalize_newlines(text: str) -> str:
|
|
67
|
+
return text.replace("\r\n", "\n").replace("\r", "\n")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _json_stable(obj: object) -> str:
|
|
71
|
+
return json.dumps(obj, sort_keys=True, separators=(",", ":"))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def sync_map(map_path: Path, desired: str) -> tuple[str, bool]:
|
|
75
|
+
"""Returns (status_token, content_mutated)."""
|
|
76
|
+
if not map_path.is_file():
|
|
77
|
+
map_path.write_text(desired, encoding="utf-8", newline="\n")
|
|
78
|
+
return "created", True
|
|
79
|
+
existing = _normalize_newlines(map_path.read_text(encoding="utf-8"))
|
|
80
|
+
if BOOTSTRAP_SENTINEL in existing:
|
|
81
|
+
if existing == desired:
|
|
82
|
+
return "noop", False
|
|
83
|
+
map_path.write_text(desired, encoding="utf-8", newline="\n")
|
|
84
|
+
return "refreshed_bootstrap", True
|
|
85
|
+
return "preserved_existing", False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def sync_dependencies(deps_path: Path, map_allows_bootstrap_deps: bool) -> tuple[str, bool]:
|
|
89
|
+
"""When map_allows_bootstrap_deps, deps may be created/updated to bootstrap empty schema."""
|
|
90
|
+
desired_txt = _bootstrap_dependencies_text()
|
|
91
|
+
want_obj = _bootstrap_dependencies_obj()
|
|
92
|
+
want_stable = _json_stable(want_obj)
|
|
93
|
+
|
|
94
|
+
if not deps_path.is_file():
|
|
95
|
+
deps_path.write_text(desired_txt, encoding="utf-8", newline="\n")
|
|
96
|
+
return "deps_created", True
|
|
97
|
+
|
|
98
|
+
raw = deps_path.read_text(encoding="utf-8")
|
|
99
|
+
try:
|
|
100
|
+
cur = json.loads(raw)
|
|
101
|
+
except json.JSONDecodeError:
|
|
102
|
+
if map_allows_bootstrap_deps:
|
|
103
|
+
deps_path.write_text(desired_txt, encoding="utf-8", newline="\n")
|
|
104
|
+
return "deps_repaired", True
|
|
105
|
+
return "deps_preserved_existing", False
|
|
106
|
+
|
|
107
|
+
if _json_stable(cur) == want_stable:
|
|
108
|
+
return "deps_noop", False
|
|
109
|
+
|
|
110
|
+
if map_allows_bootstrap_deps:
|
|
111
|
+
deps_path.write_text(desired_txt, encoding="utf-8", newline="\n")
|
|
112
|
+
return "deps_refreshed", True
|
|
113
|
+
return "deps_preserved_existing", False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def main(argv: list[str] | None = None) -> int:
|
|
117
|
+
p = argparse.ArgumentParser(description="Materialize idempotent codebase map bootstrap.")
|
|
118
|
+
p.add_argument("--repo", default=".", help="Repository root")
|
|
119
|
+
p.add_argument(
|
|
120
|
+
"--trigger",
|
|
121
|
+
choices=("architecture", "map-codebase", "refresh-context"),
|
|
122
|
+
default="architecture",
|
|
123
|
+
help="Lifecycle trigger label (stdout diagnostics only)",
|
|
124
|
+
)
|
|
125
|
+
p.add_argument("--dry-run", action="store_true", help="Print actions; do not write files")
|
|
126
|
+
p.add_argument(
|
|
127
|
+
"--simulate-block",
|
|
128
|
+
metavar="SUBREASON",
|
|
129
|
+
default=None,
|
|
130
|
+
help="Emit CODEBASE_MAP_BLOCKED:SUBREASON and exit 2 (tests)",
|
|
131
|
+
)
|
|
132
|
+
p.add_argument(
|
|
133
|
+
"--check-present",
|
|
134
|
+
action="store_true",
|
|
135
|
+
help="Read-only: exit 0 if codebase-map.md exists, else CODEBASE_MAP_MISSING + exit 2",
|
|
136
|
+
)
|
|
137
|
+
args = p.parse_args(argv)
|
|
138
|
+
|
|
139
|
+
if args.simulate_block:
|
|
140
|
+
print(f"[CODEBASE_MAP_BLOCKED:{args.simulate_block}] trigger={args.trigger}")
|
|
141
|
+
print(REMEDIATION)
|
|
142
|
+
return 2
|
|
143
|
+
|
|
144
|
+
if os.environ.get("CODEBASE_MAP_LIFECYCLE_SKIP", "").strip() in ("1", "true", "yes"):
|
|
145
|
+
print(f"[CODEBASE_MAP_BLOCKED:policy_skip] trigger={args.trigger}")
|
|
146
|
+
print(REMEDIATION)
|
|
147
|
+
return 2
|
|
148
|
+
|
|
149
|
+
root = Path(args.repo).resolve()
|
|
150
|
+
eng = root / "docs" / "engineering"
|
|
151
|
+
map_path = eng / "codebase-map.md"
|
|
152
|
+
deps_path = eng / "dependencies.json"
|
|
153
|
+
|
|
154
|
+
if args.check_present:
|
|
155
|
+
if map_path.is_file():
|
|
156
|
+
print(f"[CODEBASE_MAP_OK] check_present trigger={args.trigger} path={map_path}")
|
|
157
|
+
return 0
|
|
158
|
+
print("[CODEBASE_MAP_MISSING]")
|
|
159
|
+
print(REMEDIATION)
|
|
160
|
+
return 2
|
|
161
|
+
|
|
162
|
+
desired_map = _bootstrap_map_markdown()
|
|
163
|
+
|
|
164
|
+
if args.dry_run:
|
|
165
|
+
print(f"[CODEBASE_MAP_OK] dry_run trigger={args.trigger} map={map_path} deps={deps_path}")
|
|
166
|
+
return 0
|
|
167
|
+
|
|
168
|
+
eng.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
|
|
170
|
+
map_status, _ = sync_map(map_path, desired_map)
|
|
171
|
+
print(f"[CODEBASE_MAP_OK] {map_status} trigger={args.trigger} path={map_path}")
|
|
172
|
+
|
|
173
|
+
map_text = _normalize_newlines(map_path.read_text(encoding="utf-8"))
|
|
174
|
+
map_allows_bootstrap_deps = BOOTSTRAP_SENTINEL in map_text
|
|
175
|
+
|
|
176
|
+
dep_status, _ = sync_dependencies(deps_path, map_allows_bootstrap_deps)
|
|
177
|
+
if dep_status not in ("deps_noop", "deps_preserved_existing"):
|
|
178
|
+
print(f"[CODEBASE_MAP_OK] {dep_status} trigger={args.trigger} path={deps_path}")
|
|
179
|
+
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
raise SystemExit(main())
|