@encore-os/eos-spec 0.3.1
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/LICENSE +17 -0
- package/bin/check-dist-fresh.js +51 -0
- package/bin/eos-spec.js +7 -0
- package/dist/commands/ac-backfill.d.ts +31 -0
- package/dist/commands/ac-backfill.js +198 -0
- package/dist/commands/ac-backfill.js.map +1 -0
- package/dist/commands/archive.d.ts +2 -0
- package/dist/commands/archive.js +101 -0
- package/dist/commands/archive.js.map +1 -0
- package/dist/commands/audit.d.ts +30 -0
- package/dist/commands/audit.js +107 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/backfill.d.ts +41 -0
- package/dist/commands/backfill.js +239 -0
- package/dist/commands/backfill.js.map +1 -0
- package/dist/commands/checkArtifacts.d.ts +2 -0
- package/dist/commands/checkArtifacts.js +48 -0
- package/dist/commands/checkArtifacts.js.map +1 -0
- package/dist/commands/checkPlacement.d.ts +2 -0
- package/dist/commands/checkPlacement.js +31 -0
- package/dist/commands/checkPlacement.js.map +1 -0
- package/dist/commands/claim.d.ts +4 -0
- package/dist/commands/claim.js +46 -0
- package/dist/commands/claim.js.map +1 -0
- package/dist/commands/complete.d.ts +2 -0
- package/dist/commands/complete.js +138 -0
- package/dist/commands/complete.js.map +1 -0
- package/dist/commands/conformance.d.ts +23 -0
- package/dist/commands/conformance.js +124 -0
- package/dist/commands/conformance.js.map +1 -0
- package/dist/commands/create.d.ts +6 -0
- package/dist/commands/create.js +277 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/deferred.d.ts +2 -0
- package/dist/commands/deferred.js +85 -0
- package/dist/commands/deferred.js.map +1 -0
- package/dist/commands/delta.d.ts +17 -0
- package/dist/commands/delta.js +292 -0
- package/dist/commands/delta.js.map +1 -0
- package/dist/commands/freshness.d.ts +13 -0
- package/dist/commands/freshness.js +72 -0
- package/dist/commands/freshness.js.map +1 -0
- package/dist/commands/funnel.d.ts +2 -0
- package/dist/commands/funnel.js +61 -0
- package/dist/commands/funnel.js.map +1 -0
- package/dist/commands/gen-tests.d.ts +2 -0
- package/dist/commands/gen-tests.js +53 -0
- package/dist/commands/gen-tests.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +64 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/list.d.ts +9 -0
- package/dist/commands/list.js +61 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/metrics.d.ts +2 -0
- package/dist/commands/metrics.js +66 -0
- package/dist/commands/metrics.js.map +1 -0
- package/dist/commands/next.d.ts +48 -0
- package/dist/commands/next.js +344 -0
- package/dist/commands/next.js.map +1 -0
- package/dist/commands/precedent.d.ts +66 -0
- package/dist/commands/precedent.js +343 -0
- package/dist/commands/precedent.js.map +1 -0
- package/dist/commands/ready.d.ts +28 -0
- package/dist/commands/ready.js +147 -0
- package/dist/commands/ready.js.map +1 -0
- package/dist/commands/reconcile.d.ts +44 -0
- package/dist/commands/reconcile.js +187 -0
- package/dist/commands/reconcile.js.map +1 -0
- package/dist/commands/rollup.d.ts +2 -0
- package/dist/commands/rollup.js +43 -0
- package/dist/commands/rollup.js.map +1 -0
- package/dist/commands/scorecard.d.ts +11 -0
- package/dist/commands/scorecard.js +55 -0
- package/dist/commands/scorecard.js.map +1 -0
- package/dist/commands/signoff.d.ts +2 -0
- package/dist/commands/signoff.js +114 -0
- package/dist/commands/signoff.js.map +1 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +100 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/tasks.d.ts +13 -0
- package/dist/commands/tasks.js +81 -0
- package/dist/commands/tasks.js.map +1 -0
- package/dist/commands/validate.d.ts +16 -0
- package/dist/commands/validate.js +89 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands/verify-acs.d.ts +15 -0
- package/dist/commands/verify-acs.js +196 -0
- package/dist/commands/verify-acs.js.map +1 -0
- package/dist/commands/verifyRollout.d.ts +2 -0
- package/dist/commands/verifyRollout.js +125 -0
- package/dist/commands/verifyRollout.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +138 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/ac-draft-prompt.d.ts +18 -0
- package/dist/lib/ac-draft-prompt.js +38 -0
- package/dist/lib/ac-draft-prompt.js.map +1 -0
- package/dist/lib/ac-response-parser.d.ts +18 -0
- package/dist/lib/ac-response-parser.js +57 -0
- package/dist/lib/ac-response-parser.js.map +1 -0
- package/dist/lib/ai-call.d.ts +13 -0
- package/dist/lib/ai-call.js +17 -0
- package/dist/lib/ai-call.js.map +1 -0
- package/dist/lib/archive.d.ts +21 -0
- package/dist/lib/archive.js +136 -0
- package/dist/lib/archive.js.map +1 -0
- package/dist/lib/artifact-integrity.d.ts +5 -0
- package/dist/lib/artifact-integrity.js +65 -0
- package/dist/lib/artifact-integrity.js.map +1 -0
- package/dist/lib/backfill-queue.d.ts +11 -0
- package/dist/lib/backfill-queue.js +14 -0
- package/dist/lib/backfill-queue.js.map +1 -0
- package/dist/lib/blockers.d.ts +66 -0
- package/dist/lib/blockers.js +103 -0
- package/dist/lib/blockers.js.map +1 -0
- package/dist/lib/claims.d.ts +39 -0
- package/dist/lib/claims.js +133 -0
- package/dist/lib/claims.js.map +1 -0
- package/dist/lib/classify-debt.d.ts +17 -0
- package/dist/lib/classify-debt.js +13 -0
- package/dist/lib/classify-debt.js.map +1 -0
- package/dist/lib/code-evidence.d.ts +35 -0
- package/dist/lib/code-evidence.js +171 -0
- package/dist/lib/code-evidence.js.map +1 -0
- package/dist/lib/coherence.d.ts +14 -0
- package/dist/lib/coherence.js +188 -0
- package/dist/lib/coherence.js.map +1 -0
- package/dist/lib/completion-gate.d.ts +17 -0
- package/dist/lib/completion-gate.js +28 -0
- package/dist/lib/completion-gate.js.map +1 -0
- package/dist/lib/compliance-signoff.d.ts +39 -0
- package/dist/lib/compliance-signoff.js +81 -0
- package/dist/lib/compliance-signoff.js.map +1 -0
- package/dist/lib/config.d.ts +63 -0
- package/dist/lib/config.js +40 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/cost.d.ts +12 -0
- package/dist/lib/cost.js +12 -0
- package/dist/lib/cost.js.map +1 -0
- package/dist/lib/debt-dashboard.d.ts +25 -0
- package/dist/lib/debt-dashboard.js +36 -0
- package/dist/lib/debt-dashboard.js.map +1 -0
- package/dist/lib/embedding-store.d.ts +103 -0
- package/dist/lib/embedding-store.js +133 -0
- package/dist/lib/embedding-store.js.map +1 -0
- package/dist/lib/embeddings-source.d.ts +36 -0
- package/dist/lib/embeddings-source.js +62 -0
- package/dist/lib/embeddings-source.js.map +1 -0
- package/dist/lib/enforcement-allowlist.d.ts +12 -0
- package/dist/lib/enforcement-allowlist.js +25 -0
- package/dist/lib/enforcement-allowlist.js.map +1 -0
- package/dist/lib/events.d.ts +47 -0
- package/dist/lib/events.js +75 -0
- package/dist/lib/events.js.map +1 -0
- package/dist/lib/exec.d.ts +8 -0
- package/dist/lib/exec.js +25 -0
- package/dist/lib/exec.js.map +1 -0
- package/dist/lib/frontmatter.d.ts +112 -0
- package/dist/lib/frontmatter.js +261 -0
- package/dist/lib/frontmatter.js.map +1 -0
- package/dist/lib/funnel.d.ts +22 -0
- package/dist/lib/funnel.js +41 -0
- package/dist/lib/funnel.js.map +1 -0
- package/dist/lib/git-mtime.d.ts +4 -0
- package/dist/lib/git-mtime.js +23 -0
- package/dist/lib/git-mtime.js.map +1 -0
- package/dist/lib/metrics.d.ts +42 -0
- package/dist/lib/metrics.js +92 -0
- package/dist/lib/metrics.js.map +1 -0
- package/dist/lib/migration-collision.d.ts +20 -0
- package/dist/lib/migration-collision.js +61 -0
- package/dist/lib/migration-collision.js.map +1 -0
- package/dist/lib/next-action.d.ts +44 -0
- package/dist/lib/next-action.js +138 -0
- package/dist/lib/next-action.js.map +1 -0
- package/dist/lib/ollama-embed.d.ts +21 -0
- package/dist/lib/ollama-embed.js +65 -0
- package/dist/lib/ollama-embed.js.map +1 -0
- package/dist/lib/output.d.ts +35 -0
- package/dist/lib/output.js +46 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/pipeline-config.d.ts +20 -0
- package/dist/lib/pipeline-config.js +208 -0
- package/dist/lib/pipeline-config.js.map +1 -0
- package/dist/lib/pipeline-map.d.ts +68 -0
- package/dist/lib/pipeline-map.js +256 -0
- package/dist/lib/pipeline-map.js.map +1 -0
- package/dist/lib/pipeline-schema.d.ts +48 -0
- package/dist/lib/pipeline-schema.js +22 -0
- package/dist/lib/pipeline-schema.js.map +1 -0
- package/dist/lib/placement.d.ts +6 -0
- package/dist/lib/placement.js +40 -0
- package/dist/lib/placement.js.map +1 -0
- package/dist/lib/precedent-search.d.ts +18 -0
- package/dist/lib/precedent-search.js +160 -0
- package/dist/lib/precedent-search.js.map +1 -0
- package/dist/lib/predicate-registry.d.ts +6 -0
- package/dist/lib/predicate-registry.js +40 -0
- package/dist/lib/predicate-registry.js.map +1 -0
- package/dist/lib/recall-cases.d.ts +15 -0
- package/dist/lib/recall-cases.js +39 -0
- package/dist/lib/recall-cases.js.map +1 -0
- package/dist/lib/repo.d.ts +20 -0
- package/dist/lib/repo.js +82 -0
- package/dist/lib/repo.js.map +1 -0
- package/dist/lib/requirements.d.ts +91 -0
- package/dist/lib/requirements.js +179 -0
- package/dist/lib/requirements.js.map +1 -0
- package/dist/lib/rollup.d.ts +24 -0
- package/dist/lib/rollup.js +34 -0
- package/dist/lib/rollup.js.map +1 -0
- package/dist/lib/scorecard-run.d.ts +6 -0
- package/dist/lib/scorecard-run.js +48 -0
- package/dist/lib/scorecard-run.js.map +1 -0
- package/dist/lib/scorecard.d.ts +69 -0
- package/dist/lib/scorecard.js +113 -0
- package/dist/lib/scorecard.js.map +1 -0
- package/dist/lib/semantic-search.d.ts +36 -0
- package/dist/lib/semantic-search.js +56 -0
- package/dist/lib/semantic-search.js.map +1 -0
- package/dist/lib/session-state.d.ts +8 -0
- package/dist/lib/session-state.js +25 -0
- package/dist/lib/session-state.js.map +1 -0
- package/dist/lib/spec-debt.d.ts +8 -0
- package/dist/lib/spec-debt.js +17 -0
- package/dist/lib/spec-debt.js.map +1 -0
- package/dist/lib/spec-pure.d.ts +59 -0
- package/dist/lib/spec-pure.js +215 -0
- package/dist/lib/spec-pure.js.map +1 -0
- package/dist/lib/specs.d.ts +148 -0
- package/dist/lib/specs.js +373 -0
- package/dist/lib/specs.js.map +1 -0
- package/dist/lib/test-generator.d.ts +5 -0
- package/dist/lib/test-generator.js +72 -0
- package/dist/lib/test-generator.js.map +1 -0
- package/dist/lib/user-stories.d.ts +6 -0
- package/dist/lib/user-stories.js +56 -0
- package/dist/lib/user-stories.js.map +1 -0
- package/dist/lib/verifies-scan.d.ts +9 -0
- package/dist/lib/verifies-scan.js +73 -0
- package/dist/lib/verifies-scan.js.map +1 -0
- package/dist/lib/verify-core.d.ts +23 -0
- package/dist/lib/verify-core.js +48 -0
- package/dist/lib/verify-core.js.map +1 -0
- package/dist/lib/with-event-log.d.ts +12 -0
- package/dist/lib/with-event-log.js +65 -0
- package/dist/lib/with-event-log.js.map +1 -0
- package/dist/lib/wizard-specs.d.ts +17 -0
- package/dist/lib/wizard-specs.js +34 -0
- package/dist/lib/wizard-specs.js.map +1 -0
- package/dist/pure.d.ts +19 -0
- package/dist/pure.js +31 -0
- package/dist/pure.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { AcEntry } from './ac-response-parser.js';
|
|
2
|
+
export type { AcEntry } from './ac-response-parser.js';
|
|
3
|
+
/** Raw frontmatter shape — only the fields the pipeline navigator cares about. */
|
|
4
|
+
export type SpecFrontmatter = {
|
|
5
|
+
id?: string;
|
|
6
|
+
core?: string;
|
|
7
|
+
/** Human-readable spec title — preferred over the body `# …` heading when present. */
|
|
8
|
+
title?: string;
|
|
9
|
+
status?: string;
|
|
10
|
+
/** The id of the spec that supersedes this one. Present ⇒ terminal (C2/C3). */
|
|
11
|
+
superseded_by?: string;
|
|
12
|
+
pipeline_status?: string;
|
|
13
|
+
research_appendix?: string;
|
|
14
|
+
acceptance_criteria?: unknown[];
|
|
15
|
+
user_stories?: unknown[];
|
|
16
|
+
/** Typed functional requirements (FR-*) — consumed by `verify-acs` alongside AC/US. */
|
|
17
|
+
requirements?: unknown[];
|
|
18
|
+
e2e_tests?: unknown[];
|
|
19
|
+
/** Globs/paths the spec expects to touch — used to detect UI-bearing specs. */
|
|
20
|
+
affected_files?: unknown[];
|
|
21
|
+
tags?: unknown[];
|
|
22
|
+
/** Canonical machine-readable cross-spec blockers (list of spec IDs). May be null/dirty. */
|
|
23
|
+
blocked_by?: unknown;
|
|
24
|
+
/** Human-authored "builds on / related" list — historically dirty (prose + emoji). */
|
|
25
|
+
dependencies?: unknown;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Surgically set `pipeline_status` inside the leading frontmatter block without
|
|
29
|
+
* reformatting the rest of the YAML. Replaces the existing line, or appends one.
|
|
30
|
+
* Returns the text unchanged when there is no frontmatter block.
|
|
31
|
+
*/
|
|
32
|
+
export declare function setPipelineStatus(text: string, status: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Idempotently set `artifacts.tasks: <rel>` inside the leading frontmatter block.
|
|
35
|
+
* Replaces an existing `tasks:` child under `artifacts:`, inserts one under an
|
|
36
|
+
* existing `artifacts:` block, or appends a fresh `artifacts:` block. Returns the
|
|
37
|
+
* text unchanged when already correct or when there is no frontmatter block.
|
|
38
|
+
*
|
|
39
|
+
* Closes the `pipeline/missing-tasks-artifact` validator gap: `reconcile`
|
|
40
|
+
* materializes `pipeline_status: tasks_generated` from the on-disk TASKS file, so
|
|
41
|
+
* it must also record `artifacts.tasks` (which the pipeline-status validator
|
|
42
|
+
* requires) rather than leaving it to be hand-added.
|
|
43
|
+
*/
|
|
44
|
+
export declare function setTasksArtifact(text: string, rel: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Decide which relative path `reconcile` should write into `artifacts.tasks`, or
|
|
47
|
+
* `null` to mean "leave the frontmatter untouched".
|
|
48
|
+
*
|
|
49
|
+
* Non-destructive of valid pointers: many real specs use a non-canonical TASKS
|
|
50
|
+
* filename (e.g. `HR-02-credentialing-compliance-TASKS.md`,
|
|
51
|
+
* `<id>-PHASE-2-3-TASKS.md`). The earlier behavior unconditionally normalized
|
|
52
|
+
* `artifacts.tasks` to the canonical `<id>-TASKS.md` guess, rewriting a VALID
|
|
53
|
+
* pointer to a NON-EXISTENT path and tripping `pipeline/missing-tasks-artifact`.
|
|
54
|
+
*
|
|
55
|
+
* Rules (`fileExists(rel)` resolves a repo-relative path against repoRoot):
|
|
56
|
+
* - No frontmatter block → `null` (nothing to do).
|
|
57
|
+
* - Existing pointer whose target EXISTS → `null` (preserve — canonical or not).
|
|
58
|
+
* - Pointer ABSENT and the canonical file EXISTS → return the canonical path.
|
|
59
|
+
* - Pointer STALE (target missing) and the canonical file EXISTS → return the
|
|
60
|
+
* canonical path (heal the dangling pointer).
|
|
61
|
+
* - Otherwise (no pointer + no canonical file, or stale pointer + no canonical
|
|
62
|
+
* file) → `null`; the gate, not reconcile, surfaces the genuine gap.
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveTasksArtifact(text: string, canonicalRel: string, fileExists: (rel: string) => boolean): string | null;
|
|
65
|
+
/** Kinds of artifact-derived sign-off markers the navigator detects.
|
|
66
|
+
* `compliance` is file-based (a `{ID}-COMPLIANCE-SIGNOFF.md`), not a body comment. */
|
|
67
|
+
export type SignoffType = 'validated' | 'spec-reviewed' | 'ux-reviewed' | 'heal-verified' | 'docshots' | 'ui-gate-waived' | 'submit-e2e-waived' | 'compliance';
|
|
68
|
+
/**
|
|
69
|
+
* Insert an HTML-comment sign-off marker (e.g. `<!-- validated: 2026-05-28 -->`)
|
|
70
|
+
* immediately after the spec's frontmatter block, or at the start of the file when
|
|
71
|
+
* none exists. Idempotent: when a marker of the same type already exists, returns
|
|
72
|
+
* the text unchanged and `inserted: false`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function insertSignoffMarker(text: string, type: SignoffType, date: string): {
|
|
75
|
+
text: string;
|
|
76
|
+
inserted: boolean;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Parse the leading YAML frontmatter block of a spec markdown string.
|
|
80
|
+
* Returns `{}` when absent or malformed — never throws.
|
|
81
|
+
*/
|
|
82
|
+
export declare function readFrontmatter(text: string): SpecFrontmatter;
|
|
83
|
+
/**
|
|
84
|
+
* Remove the leading YAML frontmatter block (when present) so body-only regexes
|
|
85
|
+
* (the `# Title` heading, `**Status:**` prose) cannot match lines inside the
|
|
86
|
+
* frontmatter — a YAML comment like `# Replace placeholder values when
|
|
87
|
+
* authoring.` (SPEC_TEMPLATE.md line 2) is indistinguishable from an H1 by
|
|
88
|
+
* line shape alone.
|
|
89
|
+
*/
|
|
90
|
+
export declare function stripFrontmatter(text: string): string;
|
|
91
|
+
/**
|
|
92
|
+
* Append AC entries to the frontmatter's `acceptance_criteria:` array. Preserves
|
|
93
|
+
* other frontmatter via a slice → re-serialize-only-the-AC-list strategy. Returns
|
|
94
|
+
* the input unchanged when there is no frontmatter block.
|
|
95
|
+
*/
|
|
96
|
+
export declare function appendAcceptanceCriteria(text: string, entries: AcEntry[]): string;
|
|
97
|
+
/**
|
|
98
|
+
* Within the leading frontmatter block, flip the `status:` of one list item
|
|
99
|
+
* (under `acceptance_criteria:` / `user_stories:` / `requirements:`) identified
|
|
100
|
+
* by its `- id: <id>` line. Line-scoped so all other lines — comments, ordering,
|
|
101
|
+
* sibling items — are preserved byte-for-byte. Returns the text unchanged when
|
|
102
|
+
* there is no frontmatter, the section/id is absent, or the status already matches.
|
|
103
|
+
* Used by `verify-acs` to auto-promote planned -> verified on a real test pass.
|
|
104
|
+
*/
|
|
105
|
+
export declare function setRequirementStatus(text: string, section: 'acceptance_criteria' | 'user_stories' | 'requirements', id: string, status: 'planned' | 'verified'): string;
|
|
106
|
+
/**
|
|
107
|
+
* Idempotently add `rel` to the `e2e_tests:` list inside the leading frontmatter block.
|
|
108
|
+
* Appends a fresh `e2e_tests:` block when absent. Returns the text unchanged when
|
|
109
|
+
* the path is already listed or there is no frontmatter block. Used by `gen-tests
|
|
110
|
+
* --update-frontmatter`.
|
|
111
|
+
*/
|
|
112
|
+
export declare function setE2eTests(text: string, rel: string): string;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { parse, stringify as yamlStringify } from 'yaml';
|
|
2
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
3
|
+
/**
|
|
4
|
+
* Surgically set `pipeline_status` inside the leading frontmatter block without
|
|
5
|
+
* reformatting the rest of the YAML. Replaces the existing line, or appends one.
|
|
6
|
+
* Returns the text unchanged when there is no frontmatter block.
|
|
7
|
+
*/
|
|
8
|
+
export function setPipelineStatus(text, status) {
|
|
9
|
+
const match = FRONTMATTER_RE.exec(text);
|
|
10
|
+
if (!match)
|
|
11
|
+
return text;
|
|
12
|
+
const block = match[1];
|
|
13
|
+
const line = `pipeline_status: ${status}`;
|
|
14
|
+
const newBlock = /^pipeline_status:.*$/m.test(block)
|
|
15
|
+
? block.replace(/^pipeline_status:.*$/m, line)
|
|
16
|
+
: `${block}\n${line}`;
|
|
17
|
+
// function replacer avoids `$` in newBlock being treated as a backreference
|
|
18
|
+
return text.replace(FRONTMATTER_RE, () => `---\n${newBlock}\n---`);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Idempotently set `artifacts.tasks: <rel>` inside the leading frontmatter block.
|
|
22
|
+
* Replaces an existing `tasks:` child under `artifacts:`, inserts one under an
|
|
23
|
+
* existing `artifacts:` block, or appends a fresh `artifacts:` block. Returns the
|
|
24
|
+
* text unchanged when already correct or when there is no frontmatter block.
|
|
25
|
+
*
|
|
26
|
+
* Closes the `pipeline/missing-tasks-artifact` validator gap: `reconcile`
|
|
27
|
+
* materializes `pipeline_status: tasks_generated` from the on-disk TASKS file, so
|
|
28
|
+
* it must also record `artifacts.tasks` (which the pipeline-status validator
|
|
29
|
+
* requires) rather than leaving it to be hand-added.
|
|
30
|
+
*/
|
|
31
|
+
export function setTasksArtifact(text, rel) {
|
|
32
|
+
const match = FRONTMATTER_RE.exec(text);
|
|
33
|
+
if (!match)
|
|
34
|
+
return text;
|
|
35
|
+
const block = match[1];
|
|
36
|
+
const lines = block.split('\n');
|
|
37
|
+
const artIdx = lines.findIndex((l) => /^artifacts:\s*$/.test(l));
|
|
38
|
+
let newLines;
|
|
39
|
+
if (artIdx === -1) {
|
|
40
|
+
newLines = [...lines, 'artifacts:', ` tasks: ${rel}`];
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// Span of contiguous indented children directly under `artifacts:`.
|
|
44
|
+
let end = artIdx + 1;
|
|
45
|
+
while (end < lines.length && /^[ \t]+\S/.test(lines[end]))
|
|
46
|
+
end++;
|
|
47
|
+
const indent = lines[artIdx + 1]?.match(/^([ \t]+)/)?.[1] ?? ' ';
|
|
48
|
+
const childOffset = lines.slice(artIdx + 1, end).findIndex((l) => /^[ \t]+tasks:/.test(l));
|
|
49
|
+
if (childOffset === -1) {
|
|
50
|
+
newLines = [...lines.slice(0, artIdx + 1), `${indent}tasks: ${rel}`, ...lines.slice(artIdx + 1)];
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const absIdx = artIdx + 1 + childOffset;
|
|
54
|
+
const curIndent = lines[absIdx].match(/^([ \t]+)/)?.[1] ?? indent;
|
|
55
|
+
newLines = [...lines];
|
|
56
|
+
newLines[absIdx] = `${curIndent}tasks: ${rel}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const newBlock = newLines.join('\n');
|
|
60
|
+
if (newBlock === block)
|
|
61
|
+
return text;
|
|
62
|
+
return text.replace(FRONTMATTER_RE, () => `---\n${newBlock}\n---`);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Decide which relative path `reconcile` should write into `artifacts.tasks`, or
|
|
66
|
+
* `null` to mean "leave the frontmatter untouched".
|
|
67
|
+
*
|
|
68
|
+
* Non-destructive of valid pointers: many real specs use a non-canonical TASKS
|
|
69
|
+
* filename (e.g. `HR-02-credentialing-compliance-TASKS.md`,
|
|
70
|
+
* `<id>-PHASE-2-3-TASKS.md`). The earlier behavior unconditionally normalized
|
|
71
|
+
* `artifacts.tasks` to the canonical `<id>-TASKS.md` guess, rewriting a VALID
|
|
72
|
+
* pointer to a NON-EXISTENT path and tripping `pipeline/missing-tasks-artifact`.
|
|
73
|
+
*
|
|
74
|
+
* Rules (`fileExists(rel)` resolves a repo-relative path against repoRoot):
|
|
75
|
+
* - No frontmatter block → `null` (nothing to do).
|
|
76
|
+
* - Existing pointer whose target EXISTS → `null` (preserve — canonical or not).
|
|
77
|
+
* - Pointer ABSENT and the canonical file EXISTS → return the canonical path.
|
|
78
|
+
* - Pointer STALE (target missing) and the canonical file EXISTS → return the
|
|
79
|
+
* canonical path (heal the dangling pointer).
|
|
80
|
+
* - Otherwise (no pointer + no canonical file, or stale pointer + no canonical
|
|
81
|
+
* file) → `null`; the gate, not reconcile, surfaces the genuine gap.
|
|
82
|
+
*/
|
|
83
|
+
export function resolveTasksArtifact(text, canonicalRel, fileExists) {
|
|
84
|
+
const match = FRONTMATTER_RE.exec(text);
|
|
85
|
+
if (!match)
|
|
86
|
+
return null;
|
|
87
|
+
const fm = readFrontmatter(text);
|
|
88
|
+
const artifacts = fm && typeof fm === 'object' ? fm.artifacts : undefined;
|
|
89
|
+
const existing = artifacts && typeof artifacts === 'object' ? artifacts.tasks : undefined;
|
|
90
|
+
const current = typeof existing === 'string' && existing.trim() !== '' ? existing.trim() : undefined;
|
|
91
|
+
// Existing pointer that resolves to a real file: preserve it (canonical or not).
|
|
92
|
+
if (current && fileExists(current))
|
|
93
|
+
return null;
|
|
94
|
+
// Absent or stale pointer: only adopt the canonical guess if it actually exists.
|
|
95
|
+
if (fileExists(canonicalRel))
|
|
96
|
+
return canonicalRel;
|
|
97
|
+
// No valid target anywhere — leave the gate to flag the genuine gap.
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Insert an HTML-comment sign-off marker (e.g. `<!-- validated: 2026-05-28 -->`)
|
|
102
|
+
* immediately after the spec's frontmatter block, or at the start of the file when
|
|
103
|
+
* none exists. Idempotent: when a marker of the same type already exists, returns
|
|
104
|
+
* the text unchanged and `inserted: false`.
|
|
105
|
+
*/
|
|
106
|
+
export function insertSignoffMarker(text, type, date) {
|
|
107
|
+
const markerRe = new RegExp(`<!--\\s*${type}:`, 'i');
|
|
108
|
+
if (markerRe.test(text))
|
|
109
|
+
return { text, inserted: false };
|
|
110
|
+
const marker = `<!-- ${type}: ${date} -->`;
|
|
111
|
+
const match = FRONTMATTER_RE.exec(text);
|
|
112
|
+
if (!match) {
|
|
113
|
+
return { text: `${marker}\n${text}`, inserted: true };
|
|
114
|
+
}
|
|
115
|
+
const blockEnd = match.index + match[0].length;
|
|
116
|
+
const before = text.slice(0, blockEnd);
|
|
117
|
+
const after = text.slice(blockEnd);
|
|
118
|
+
const sep = after.startsWith('\n') ? '' : '\n';
|
|
119
|
+
return { text: `${before}${sep}\n${marker}\n${after.replace(/^\n+/, '')}`, inserted: true };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Parse the leading YAML frontmatter block of a spec markdown string.
|
|
123
|
+
* Returns `{}` when absent or malformed — never throws.
|
|
124
|
+
*/
|
|
125
|
+
export function readFrontmatter(text) {
|
|
126
|
+
const match = FRONTMATTER_RE.exec(text);
|
|
127
|
+
if (!match)
|
|
128
|
+
return {};
|
|
129
|
+
try {
|
|
130
|
+
const parsed = parse(match[1]);
|
|
131
|
+
if (parsed && typeof parsed === 'object')
|
|
132
|
+
return parsed;
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Remove the leading YAML frontmatter block (when present) so body-only regexes
|
|
141
|
+
* (the `# Title` heading, `**Status:**` prose) cannot match lines inside the
|
|
142
|
+
* frontmatter — a YAML comment like `# Replace placeholder values when
|
|
143
|
+
* authoring.` (SPEC_TEMPLATE.md line 2) is indistinguishable from an H1 by
|
|
144
|
+
* line shape alone.
|
|
145
|
+
*/
|
|
146
|
+
export function stripFrontmatter(text) {
|
|
147
|
+
return text.replace(FRONTMATTER_RE, '');
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Append AC entries to the frontmatter's `acceptance_criteria:` array. Preserves
|
|
151
|
+
* other frontmatter via a slice → re-serialize-only-the-AC-list strategy. Returns
|
|
152
|
+
* the input unchanged when there is no frontmatter block.
|
|
153
|
+
*/
|
|
154
|
+
export function appendAcceptanceCriteria(text, entries) {
|
|
155
|
+
if (entries.length === 0)
|
|
156
|
+
return text;
|
|
157
|
+
const match = FRONTMATTER_RE.exec(text);
|
|
158
|
+
if (!match)
|
|
159
|
+
return text;
|
|
160
|
+
const block = match[1];
|
|
161
|
+
const hasKey = /^acceptance_criteria\s*:/m.test(block);
|
|
162
|
+
let newBlock;
|
|
163
|
+
if (hasKey) {
|
|
164
|
+
const parsed = readFrontmatter(text);
|
|
165
|
+
const existing = Array.isArray(parsed.acceptance_criteria) ? parsed.acceptance_criteria : [];
|
|
166
|
+
const merged = yamlStringify({ acceptance_criteria: [...existing, ...entries] }).trimEnd();
|
|
167
|
+
// Replace the `acceptance_criteria:` block: from that line through the next
|
|
168
|
+
// top-level key OR the end of the frontmatter block.
|
|
169
|
+
newBlock = block.replace(/^acceptance_criteria\s*:[\s\S]*?(?=^[A-Za-z_][A-Za-z0-9_]*\s*:|\s*$)/m, `${merged}\n`);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
newBlock = `${block}\n${yamlStringify({ acceptance_criteria: entries }).trimEnd()}`;
|
|
173
|
+
}
|
|
174
|
+
return text.replace(FRONTMATTER_RE, () => `---\n${newBlock}\n---`);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Within the leading frontmatter block, flip the `status:` of one list item
|
|
178
|
+
* (under `acceptance_criteria:` / `user_stories:` / `requirements:`) identified
|
|
179
|
+
* by its `- id: <id>` line. Line-scoped so all other lines — comments, ordering,
|
|
180
|
+
* sibling items — are preserved byte-for-byte. Returns the text unchanged when
|
|
181
|
+
* there is no frontmatter, the section/id is absent, or the status already matches.
|
|
182
|
+
* Used by `verify-acs` to auto-promote planned -> verified on a real test pass.
|
|
183
|
+
*/
|
|
184
|
+
export function setRequirementStatus(text, section, id, status) {
|
|
185
|
+
const match = FRONTMATTER_RE.exec(text);
|
|
186
|
+
if (!match)
|
|
187
|
+
return text;
|
|
188
|
+
const lines = match[1].split('\n');
|
|
189
|
+
const sectionIdx = lines.findIndex((l) => new RegExp(`^${section}:\\s*$`).test(l));
|
|
190
|
+
if (sectionIdx === -1)
|
|
191
|
+
return text;
|
|
192
|
+
const escId = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
193
|
+
const idRe = new RegExp(`^\\s*-\\s+id:\\s*["']?${escId}["']?\\s*$`);
|
|
194
|
+
let itemIdx = -1;
|
|
195
|
+
for (let i = sectionIdx + 1; i < lines.length; i++) {
|
|
196
|
+
if (/^\S/.test(lines[i]))
|
|
197
|
+
break; // next column-0 key ends the section
|
|
198
|
+
if (idRe.test(lines[i])) {
|
|
199
|
+
itemIdx = i;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (itemIdx === -1)
|
|
204
|
+
return text;
|
|
205
|
+
let statusIdx = -1;
|
|
206
|
+
for (let i = itemIdx + 1; i < lines.length; i++) {
|
|
207
|
+
if (/^\S/.test(lines[i]) || /^\s*-\s+id:/.test(lines[i]))
|
|
208
|
+
break; // next item or section end
|
|
209
|
+
if (/^\s*status:\s*/.test(lines[i])) {
|
|
210
|
+
statusIdx = i;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const indent = lines[itemIdx].match(/^(\s*)-\s/)?.[1] ?? ' ';
|
|
215
|
+
const statusLine = `${indent} status: ${status}`;
|
|
216
|
+
let newLines;
|
|
217
|
+
if (statusIdx !== -1) {
|
|
218
|
+
if (lines[statusIdx].trim() === `status: ${status}`)
|
|
219
|
+
return text;
|
|
220
|
+
newLines = [...lines];
|
|
221
|
+
newLines[statusIdx] = statusLine;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
newLines = [...lines.slice(0, itemIdx + 1), statusLine, ...lines.slice(itemIdx + 1)];
|
|
225
|
+
}
|
|
226
|
+
const newBlock = newLines.join('\n');
|
|
227
|
+
if (newBlock === match[1])
|
|
228
|
+
return text;
|
|
229
|
+
return text.replace(FRONTMATTER_RE, () => `---\n${newBlock}\n---`);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Idempotently add `rel` to the `e2e_tests:` list inside the leading frontmatter block.
|
|
233
|
+
* Appends a fresh `e2e_tests:` block when absent. Returns the text unchanged when
|
|
234
|
+
* the path is already listed or there is no frontmatter block. Used by `gen-tests
|
|
235
|
+
* --update-frontmatter`.
|
|
236
|
+
*/
|
|
237
|
+
export function setE2eTests(text, rel) {
|
|
238
|
+
const match = FRONTMATTER_RE.exec(text);
|
|
239
|
+
if (!match)
|
|
240
|
+
return text;
|
|
241
|
+
const block = match[1];
|
|
242
|
+
const lines = block.split('\n');
|
|
243
|
+
const keyIdx = lines.findIndex((l) => /^e2e_tests:\s*$/.test(l));
|
|
244
|
+
let newLines;
|
|
245
|
+
if (keyIdx === -1) {
|
|
246
|
+
newLines = [...lines, 'e2e_tests:', ` - ${rel}`];
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
let end = keyIdx + 1;
|
|
250
|
+
while (end < lines.length && /^[ \t]+-\s/.test(lines[end]))
|
|
251
|
+
end++;
|
|
252
|
+
if (lines.slice(keyIdx + 1, end).some((l) => l.trim() === `- ${rel}`))
|
|
253
|
+
return text;
|
|
254
|
+
newLines = [...lines.slice(0, end), ` - ${rel}`, ...lines.slice(end)];
|
|
255
|
+
}
|
|
256
|
+
const newBlock = newLines.join('\n');
|
|
257
|
+
if (newBlock === block)
|
|
258
|
+
return text;
|
|
259
|
+
return text.replace(FRONTMATTER_RE, () => `---\n${newBlock}\n---`);
|
|
260
|
+
}
|
|
261
|
+
//# sourceMappingURL=frontmatter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontmatter.js","sourceRoot":"","sources":["../../src/lib/frontmatter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,MAAM,CAAC;AA8BzD,MAAM,cAAc,GAAG,6BAA6B,CAAC;AAErD;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,MAAc;IAC5D,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;IACxB,MAAM,IAAI,GAAG,oBAAoB,MAAM,EAAE,CAAC;IAC1C,MAAM,QAAQ,GAAG,uBAAuB,CAAC,IAAI,CAAC,KAAK,CAAC;QAClD,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,uBAAuB,EAAE,IAAI,CAAC;QAC9C,CAAC,CAAC,GAAG,KAAK,KAAK,IAAI,EAAE,CAAC;IACxB,4EAA4E;IAC5E,OAAO,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,QAAQ,QAAQ,OAAO,CAAC,CAAC;AACrE,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,GAAW;IACxD,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;IACxB,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,MAAM,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAEjE,IAAI,QAAkB,CAAC;IACvB,IAAI,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;QAClB,QAAQ,GAAG,CAAC,GAAG,KAAK,EAAE,YAAY,EAAE,YAAY,GAAG,EAAE,CAAC,CAAC;IACzD,CAAC;SAAM,CAAC;QACN,oEAAoE;QACpE,IAAI,GAAG,GAAG,MAAM,GAAG,CAAC,CAAC;QACrB,OAAO,GAAG,GAAG,KAAK,CAAC,MAAM,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAE,CAAC;YAAE,GAAG,EAAE,CAAC;QAClE,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;QAClE,MAAM,WAAW,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3F,IAAI,WAAW,KAAK,CAAC,CAAC,EAAE,CAAC;YACvB,QAAQ,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,GAAG,MAAM,UAAU,GAAG,EAAE,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;QACnG,CAAC;aAAM,CAAC;YACN,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,GAAG,WAAW,CAAC;YACxC,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAE,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC;YACnE,QAAQ,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;YACtB,QAAQ,CAAC,MAAM,CAAC,GAAG,GAAG,SAAS,UAAU,GAAG,EAAE,CAAC;QACjD,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrC,IAAI,QAAQ,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IACpC,OAAO,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,QAAQ,QAAQ,OAAO,CAAC,CAAC;AACrE,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAAY,EACZ,YAAoB,EACpB,UAAoC;IAEpC,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,EAAE,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,SAAS,GAAG,EAAE,IAAI,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAE,EAA8B,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;IACvG,MAAM,QAAQ,GAAG,SAAS,IAAI,OAAO,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAE,SAAiC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IACnH,MAAM,OAAO,GAAG,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAErG,iFAAiF;IACjF,IAAI,OAAO,IAAI,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IAChD,iFAAiF;IACjF,IAAI,UAAU,CAAC,YAAY,CAAC;QAAE,OAAO,YAAY,CAAC;IAClD,qEAAqE;IACrE,OAAO,IAAI,CAAC;AACd,CAAC;AAcD;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAAY,EACZ,IAAiB,EACjB,IAAY;IAEZ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,WAAW,IAAI,GAAG,EAAE,GAAG,CAAC,CAAC;IACrD,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAC1D,MAAM,MAAM,GAAG,QAAQ,IAAI,KAAK,IAAI,MAAM,CAAC;IAC3C,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,EAAE,IAAI,EAAE,GAAG,MAAM,KAAK,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACxD,CAAC;IACD,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACnC,MAAM,GAAG,GAAG,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/C,OAAO,EAAE,IAAI,EAAE,GAAG,MAAM,GAAG,GAAG,KAAK,MAAM,KAAK,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAC9F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IACtB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAE,CAAY,CAAC;QAC3C,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO,MAAyB,CAAC;QAC3E,OAAO,EAAE,CAAC;IACZ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;AAC1C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAAY,EAAE,OAAkB;IACvE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;IACxB,MAAM,MAAM,GAAG,2BAA2B,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvD,IAAI,QAAgB,CAAC;IACrB,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;QACrC,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAE,MAAM,CAAC,mBAAiC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5G,MAAM,MAAM,GAAG,aAAa,CAAC,EAAE,mBAAmB,EAAE,CAAC,GAAG,QAAQ,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;QAC3F,4EAA4E;QAC5E,qDAAqD;QACrD,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,uEAAuE,EAAE,GAAG,MAAM,IAAI,CAAC,CAAC;IACnH,CAAC;SAAM,CAAC;QACN,QAAQ,GAAG,GAAG,KAAK,KAAK,aAAa,CAAC,EAAE,mBAAmB,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;IACtF,CAAC;IACD,OAAO,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,QAAQ,QAAQ,OAAO,CAAC,CAAC;AACrE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAAY,EACZ,OAAgE,EAChE,EAAU,EACV,MAA8B;IAE9B,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAEpC,MAAM,UAAU,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,IAAI,OAAO,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACnF,IAAI,UAAU,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAEnC,MAAM,KAAK,GAAG,EAAE,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;IACxD,MAAM,IAAI,GAAG,IAAI,MAAM,CAAC,yBAAyB,KAAK,YAAY,CAAC,CAAC;IACpE,IAAI,OAAO,GAAG,CAAC,CAAC,CAAC;IACjB,KAAK,IAAI,CAAC,GAAG,UAAU,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACnD,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC;YAAE,MAAM,CAAC,qCAAqC;QACvE,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,EAAE,CAAC;YACzB,OAAO,GAAG,CAAC,CAAC;YACZ,MAAM;QACR,CAAC;IACH,CAAC;IACD,IAAI,OAAO,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAEhC,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC;IACnB,KAAK,IAAI,CAAC,GAAG,OAAO,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChD,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC;YAAE,MAAM,CAAC,2BAA2B;QAC9F,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,EAAE,CAAC;YACrC,SAAS,GAAG,CAAC,CAAC;YACd,MAAM;QACR,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAE,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IAC/D,MAAM,UAAU,GAAG,GAAG,MAAM,aAAa,MAAM,EAAE,CAAC;IAElD,IAAI,QAAkB,CAAC;IACvB,IAAI,SAAS,KAAK,CAAC,CAAC,EAAE,CAAC;QACrB,IAAI,KAAK,CAAC,SAAS,CAAE,CAAC,IAAI,EAAE,KAAK,WAAW,MAAM,EAAE;YAAE,OAAO,IAAI,CAAC;QAClE,QAAQ,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;QACtB,QAAQ,CAAC,SAAS,CAAC,GAAG,UAAU,CAAC;IACnC,CAAC;SAAM,CAAC;QACN,QAAQ,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;IACvF,CAAC;IACD,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrC,IAAI,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACvC,OAAO,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,QAAQ,QAAQ,OAAO,CAAC,CAAC;AACrE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,GAAW;IACnD,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;IACxB,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,MAAM,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACjE,IAAI,QAAkB,CAAC;IACvB,IAAI,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;QAClB,QAAQ,GAAG,CAAC,GAAG,KAAK,EAAE,YAAY,EAAE,OAAO,GAAG,EAAE,CAAC,CAAC;IACpD,CAAC;SAAM,CAAC;QACN,IAAI,GAAG,GAAG,MAAM,GAAG,CAAC,CAAC;QACrB,OAAO,GAAG,GAAG,KAAK,CAAC,MAAM,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAE,CAAC;YAAE,GAAG,EAAE,CAAC;QACnE,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,KAAK,GAAG,EAAE,CAAC;YAAE,OAAO,IAAI,CAAC;QACnF,QAAQ,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,OAAO,GAAG,EAAE,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACzE,CAAC;IACD,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrC,IAAI,QAAQ,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IACpC,OAAO,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,QAAQ,QAAQ,OAAO,CAAC,CAAC;AACrE,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SpecFile } from './specs.js';
|
|
2
|
+
export type FunnelInput = {
|
|
3
|
+
specs: SpecFile[];
|
|
4
|
+
/** Map spec.path → last-modified epoch-ms (from git). Missing entry = treat as fresh. */
|
|
5
|
+
mtimeBySpecPath: Record<string, number>;
|
|
6
|
+
stallDays: number;
|
|
7
|
+
/** Override of Date.now() for testability. */
|
|
8
|
+
now?: number;
|
|
9
|
+
};
|
|
10
|
+
export type FunnelReport = {
|
|
11
|
+
total: number;
|
|
12
|
+
byStage: Record<string, number>;
|
|
13
|
+
withStories: number;
|
|
14
|
+
withACs: number;
|
|
15
|
+
storiedButNoACs: number;
|
|
16
|
+
/** Advisory (canonical seed-fixture Phase 3): specs that declare a Lane-4 seed fixture. */
|
|
17
|
+
withSeedFixtures: number;
|
|
18
|
+
/** Specs with ACs but no declared seed fixture — the "ships rows, no fixture" gap signal. */
|
|
19
|
+
acsButNoSeedFixture: number;
|
|
20
|
+
stalls: number;
|
|
21
|
+
};
|
|
22
|
+
export declare function computeFunnel(input: FunnelInput): FunnelReport;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { computeStage } from './pipeline-map.js';
|
|
2
|
+
/** Treat tasks_generated as terminal for stall purposes. */
|
|
3
|
+
const TERMINAL_STAGES = new Set(['tasks_generated']);
|
|
4
|
+
export function computeFunnel(input) {
|
|
5
|
+
const now = input.now ?? Date.now();
|
|
6
|
+
const cutoff = now - input.stallDays * 24 * 60 * 60 * 1000;
|
|
7
|
+
const byStage = {};
|
|
8
|
+
let withStories = 0, withACs = 0, storied = 0, withSeedFixtures = 0, acsButNoSeedFixture = 0, stalls = 0;
|
|
9
|
+
for (const s of input.specs) {
|
|
10
|
+
const stage = computeStage(s);
|
|
11
|
+
byStage[stage] = (byStage[stage] ?? 0) + 1;
|
|
12
|
+
if (s.signals.hasUserStories)
|
|
13
|
+
withStories++;
|
|
14
|
+
if (s.signals.hasAcceptanceCriteria)
|
|
15
|
+
withACs++;
|
|
16
|
+
if (s.signals.hasUserStories && !s.signals.hasAcceptanceCriteria)
|
|
17
|
+
storied++;
|
|
18
|
+
if (s.signals.hasSeedFixture)
|
|
19
|
+
withSeedFixtures++;
|
|
20
|
+
// "Ships rows, no fixture" proxy: a spec mature enough to have ACs but with no
|
|
21
|
+
// declared seed fixture — its surfaces likely render empty in E2E/docs.
|
|
22
|
+
if (s.signals.hasAcceptanceCriteria && !s.signals.hasSeedFixture)
|
|
23
|
+
acsButNoSeedFixture++;
|
|
24
|
+
const mtime = input.mtimeBySpecPath[s.path];
|
|
25
|
+
// A spec is terminal (not stalled) if its computed stage is terminal OR if it already has tasks.
|
|
26
|
+
const isTerminal = TERMINAL_STAGES.has(stage) || s.hasTasks;
|
|
27
|
+
if (typeof mtime === 'number' && mtime <= cutoff && !isTerminal)
|
|
28
|
+
stalls++;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
total: input.specs.length,
|
|
32
|
+
byStage,
|
|
33
|
+
withStories,
|
|
34
|
+
withACs,
|
|
35
|
+
storiedButNoACs: storied,
|
|
36
|
+
withSeedFixtures,
|
|
37
|
+
acsButNoSeedFixture,
|
|
38
|
+
stalls,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=funnel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"funnel.js","sourceRoot":"","sources":["../../src/lib/funnel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAyBjD,4DAA4D;AAC5D,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC;AAErD,MAAM,UAAU,aAAa,CAAC,KAAkB;IAC9C,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IACpC,MAAM,MAAM,GAAG,GAAG,GAAG,KAAK,CAAC,SAAS,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC3D,MAAM,OAAO,GAA2B,EAAE,CAAC;IAC3C,IAAI,WAAW,GAAG,CAAC,EACjB,OAAO,GAAG,CAAC,EACX,OAAO,GAAG,CAAC,EACX,gBAAgB,GAAG,CAAC,EACpB,mBAAmB,GAAG,CAAC,EACvB,MAAM,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;QAC9B,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,CAAC,OAAO,CAAC,cAAc;YAAE,WAAW,EAAE,CAAC;QAC5C,IAAI,CAAC,CAAC,OAAO,CAAC,qBAAqB;YAAE,OAAO,EAAE,CAAC;QAC/C,IAAI,CAAC,CAAC,OAAO,CAAC,cAAc,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,qBAAqB;YAAE,OAAO,EAAE,CAAC;QAC5E,IAAI,CAAC,CAAC,OAAO,CAAC,cAAc;YAAE,gBAAgB,EAAE,CAAC;QACjD,+EAA+E;QAC/E,wEAAwE;QACxE,IAAI,CAAC,CAAC,OAAO,CAAC,qBAAqB,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc;YAAE,mBAAmB,EAAE,CAAC;QACxF,MAAM,KAAK,GAAG,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC5C,iGAAiG;QACjG,MAAM,UAAU,GAAG,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC;QAC5D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,IAAI,MAAM,IAAI,CAAC,UAAU;YAAE,MAAM,EAAE,CAAC;IAC5E,CAAC;IACD,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM;QACzB,OAAO;QACP,WAAW;QACX,OAAO;QACP,eAAe,EAAE,OAAO;QACxB,gBAAgB;QAChB,mBAAmB;QACnB,MAAM;KACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Last-modified epoch-ms for each path, derived from `git log -1 --format=%ct`.
|
|
2
|
+
* Falls back silently (path omitted) when git has no history for a path. The
|
|
3
|
+
* caller treats absence as "fresh" (no stall). */
|
|
4
|
+
export declare function gitMtimes(repoRoot: string, paths: string[]): Record<string, number>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
/** Last-modified epoch-ms for each path, derived from `git log -1 --format=%ct`.
|
|
3
|
+
* Falls back silently (path omitted) when git has no history for a path. The
|
|
4
|
+
* caller treats absence as "fresh" (no stall). */
|
|
5
|
+
export function gitMtimes(repoRoot, paths) {
|
|
6
|
+
const out = {};
|
|
7
|
+
for (const p of paths) {
|
|
8
|
+
try {
|
|
9
|
+
const sec = execFileSync('git', ['log', '-1', '--format=%ct', '--', p], {
|
|
10
|
+
cwd: repoRoot,
|
|
11
|
+
encoding: 'utf8',
|
|
12
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
13
|
+
}).trim();
|
|
14
|
+
if (sec)
|
|
15
|
+
out[p] = parseInt(sec, 10) * 1000;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// path has no git history — leave it out of the map
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=git-mtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-mtime.js","sourceRoot":"","sources":["../../src/lib/git-mtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD;;mDAEmD;AACnD,MAAM,UAAU,SAAS,CAAC,QAAgB,EAAE,KAAe;IACzD,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE;gBACtE,GAAG,EAAE,QAAQ;gBACb,QAAQ,EAAE,MAAM;gBAChB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;aACpC,CAAC,CAAC,IAAI,EAAE,CAAC;YACV,IAAI,GAAG;gBAAE,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC;QAC7C,CAAC;QAAC,MAAM,CAAC;YACP,oDAAoD;QACtD,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { EosSpecEvent } from './events.js';
|
|
2
|
+
export type CommandStats = {
|
|
3
|
+
count: number;
|
|
4
|
+
wallClockMs: number;
|
|
5
|
+
successRate: number;
|
|
6
|
+
};
|
|
7
|
+
export type ModelStats = {
|
|
8
|
+
calls: number;
|
|
9
|
+
tokensIn: number;
|
|
10
|
+
tokensOut: number;
|
|
11
|
+
totalCostUsd: number | null;
|
|
12
|
+
};
|
|
13
|
+
export type HaltCauseStats = {
|
|
14
|
+
/** Halts where a governance gate fired (autonomy working as designed). */
|
|
15
|
+
expectedHalts: number;
|
|
16
|
+
/** Halts from crashes/tooling failures/stalls (genuine intervention). */
|
|
17
|
+
unexpectedHalts: number;
|
|
18
|
+
/** Count by cause key: a gate name (expected), 'crash', or 'stalled' (unexpected). */
|
|
19
|
+
byCause: Record<string, number>;
|
|
20
|
+
};
|
|
21
|
+
export type AutonomyStats = {
|
|
22
|
+
longestRun: number;
|
|
23
|
+
humanTouches: number;
|
|
24
|
+
lastHalt?: {
|
|
25
|
+
command: string;
|
|
26
|
+
error?: string;
|
|
27
|
+
ts: string;
|
|
28
|
+
};
|
|
29
|
+
byHaltCause: HaltCauseStats;
|
|
30
|
+
};
|
|
31
|
+
export type MetricsReport = {
|
|
32
|
+
total: number;
|
|
33
|
+
windowStart?: string;
|
|
34
|
+
windowEnd?: string;
|
|
35
|
+
byCommand: Record<string, CommandStats>;
|
|
36
|
+
byModel: Record<string, ModelStats>;
|
|
37
|
+
autonomy: AutonomyStats;
|
|
38
|
+
};
|
|
39
|
+
export declare function computeMetrics(events: EosSpecEvent[], opts?: {
|
|
40
|
+
since?: Date;
|
|
41
|
+
core?: string;
|
|
42
|
+
}): MetricsReport;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** D3 stall threshold. The funnel 30-day idiom is far too coarse for an
|
|
2
|
+
* intra-run heartbeat; a multi-hour gap between consecutive events with no
|
|
3
|
+
* terminating event is treated as a silent death (context exhaustion / wedged
|
|
4
|
+
* process). Tunable via EOS_SPEC_STALL_GAP_MS; default 2 hours. */
|
|
5
|
+
const STALL_GAP_MS = Number(process.env.EOS_SPEC_STALL_GAP_MS) || 2 * 60 * 60 * 1000;
|
|
6
|
+
export function computeMetrics(events, opts = {}) {
|
|
7
|
+
const filtered = events.filter((e) => {
|
|
8
|
+
if (opts.since && new Date(e.ts).getTime() < opts.since.getTime())
|
|
9
|
+
return false;
|
|
10
|
+
if (opts.core && e.core !== opts.core)
|
|
11
|
+
return false;
|
|
12
|
+
return true;
|
|
13
|
+
});
|
|
14
|
+
const byCommand = {};
|
|
15
|
+
const byModel = {};
|
|
16
|
+
let longestRun = 0;
|
|
17
|
+
let run = 0;
|
|
18
|
+
let humanTouches = 0;
|
|
19
|
+
let lastHalt;
|
|
20
|
+
let expectedHalts = 0;
|
|
21
|
+
let unexpectedHalts = 0;
|
|
22
|
+
const byCause = {};
|
|
23
|
+
const bump = (k) => {
|
|
24
|
+
byCause[k] = (byCause[k] ?? 0) + 1;
|
|
25
|
+
};
|
|
26
|
+
let prevLifecycleTs;
|
|
27
|
+
for (const e of filtered) {
|
|
28
|
+
const c = (byCommand[e.command] ??= { count: 0, ms: 0, oks: 0 });
|
|
29
|
+
c.count++;
|
|
30
|
+
c.ms += e.ms;
|
|
31
|
+
if (e.ok)
|
|
32
|
+
c.oks++;
|
|
33
|
+
if (e.model && e.tokens) {
|
|
34
|
+
const m = (byModel[e.model] ??= { calls: 0, tokensIn: 0, tokensOut: 0, cost: 0, anyNull: false });
|
|
35
|
+
m.calls++;
|
|
36
|
+
m.tokensIn += e.tokens.prompt;
|
|
37
|
+
m.tokensOut += e.tokens.completion;
|
|
38
|
+
if (e.costUsd === null || e.costUsd === undefined)
|
|
39
|
+
m.anyNull = true;
|
|
40
|
+
else
|
|
41
|
+
m.cost += e.costUsd;
|
|
42
|
+
}
|
|
43
|
+
// D1 taxonomy — a gate event (verdict:block, or an explicit halt) is an
|
|
44
|
+
// EXPECTED halt; an ok:false with no gate is an unexpected crash. A gate
|
|
45
|
+
// event with ok:true (warn/respected-pass) is neither a halt nor a touch.
|
|
46
|
+
if (e.gate !== undefined) {
|
|
47
|
+
if (e.verdict === 'block' || e.ok === false) {
|
|
48
|
+
expectedHalts++;
|
|
49
|
+
bump(e.gate);
|
|
50
|
+
}
|
|
51
|
+
// Gate events never participate in the run/human-touch counters below.
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// D2 — only command-lifecycle events (no gate) drive the run counters.
|
|
55
|
+
// D3 — a multi-hour gap between consecutive lifecycle events with no
|
|
56
|
+
// terminating event is a silent stall (unexpected halt).
|
|
57
|
+
const tsMs = new Date(e.ts).getTime();
|
|
58
|
+
if (prevLifecycleTs !== undefined && tsMs - prevLifecycleTs > STALL_GAP_MS) {
|
|
59
|
+
run = 0;
|
|
60
|
+
unexpectedHalts++;
|
|
61
|
+
bump('stalled');
|
|
62
|
+
}
|
|
63
|
+
prevLifecycleTs = tsMs;
|
|
64
|
+
if (e.ok) {
|
|
65
|
+
run++;
|
|
66
|
+
if (run > longestRun)
|
|
67
|
+
longestRun = run;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
run = 0;
|
|
71
|
+
humanTouches++;
|
|
72
|
+
unexpectedHalts++;
|
|
73
|
+
bump(e.haltCause ?? 'crash');
|
|
74
|
+
lastHalt = { command: e.command, error: e.error, ts: e.ts };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
total: filtered.length,
|
|
79
|
+
windowStart: filtered[0]?.ts,
|
|
80
|
+
windowEnd: filtered[filtered.length - 1]?.ts,
|
|
81
|
+
byCommand: Object.fromEntries(Object.entries(byCommand).map(([k, v]) => [
|
|
82
|
+
k,
|
|
83
|
+
{ count: v.count, wallClockMs: v.ms, successRate: v.oks / v.count },
|
|
84
|
+
])),
|
|
85
|
+
byModel: Object.fromEntries(Object.entries(byModel).map(([k, v]) => [
|
|
86
|
+
k,
|
|
87
|
+
{ calls: v.calls, tokensIn: v.tokensIn, tokensOut: v.tokensOut, totalCostUsd: v.anyNull ? null : v.cost },
|
|
88
|
+
])),
|
|
89
|
+
autonomy: { longestRun, humanTouches, lastHalt, byHaltCause: { expectedHalts, unexpectedHalts, byCause } },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=metrics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../src/lib/metrics.ts"],"names":[],"mappings":"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"}
|