cleargate 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/MANIFEST.json +71 -15
- package/dist/admin-api/index.cjs +0 -1
- package/dist/admin-api/index.js +1 -2
- package/dist/auth/factory.cjs +0 -1
- package/dist/auth/factory.js +2 -3
- package/dist/auth/require-token.cjs +0 -1
- package/dist/auth/require-token.js +1 -2
- package/dist/auth/token-store.cjs +0 -1
- package/dist/auth/token-store.js +1 -2
- package/dist/{bootstrap-root-QKSA5V75.js → bootstrap-root-2H5HVTCC.js} +1 -2
- package/dist/{chunk-PDE37WFQ.js → chunk-A7MSQUU7.js} +2 -3
- package/dist/{chunk-BTSZOEWC.js → chunk-P6KEDAK2.js} +0 -1
- package/dist/{chunk-E3X7IE5E.js → chunk-PY6FHGV5.js} +1 -2
- package/dist/{chunk-5DI2Z3C2.js → chunk-Y53ZZYYU.js} +1 -2
- package/dist/cli.cjs +1564 -1414
- package/dist/cli.js +1514 -1364
- package/dist/lib/ledger.cjs +0 -1
- package/dist/lib/ledger.js +1 -2
- package/dist/lib/lifecycle-reconcile.cjs +0 -1
- package/dist/lib/lifecycle-reconcile.js +2 -3
- package/dist/{whoami-EANGN46Z.js → whoami-JKQQPABQ.js} +3 -4
- package/package.json +4 -3
- package/templates/cleargate-planning/.claude/agents/architect.md +4 -2
- package/templates/cleargate-planning/.claude/agents/developer.md +4 -11
- package/templates/cleargate-planning/.claude/agents/qa.md +14 -6
- package/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +2 -2
- package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +19 -1
- package/templates/cleargate-planning/.cleargate/config.example.yml +16 -0
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.deferred-verify.red.node.test.ts +245 -0
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +227 -0
- package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +5 -4
- package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +75 -2
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +48 -0
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +57 -1
- package/templates/cleargate-planning/.cleargate/scripts/provision_worktree_config.sh +155 -0
- package/templates/cleargate-planning/.cleargate/scripts/qa_red_lint.mjs +380 -0
- package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +34 -1
- package/templates/cleargate-planning/.cleargate/scripts/test/cr077_eviction.red.sh +113 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr078_init.test.sh +309 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr079_provision.red.sh +262 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr080_wrapper.test.sh +177 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr081_qa_red_lint.red.sh +348 -0
- package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/.session-totals.json +1 -0
- package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/token-ledger.jsonl +27 -0
- package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +17 -0
- package/templates/cleargate-planning/.cleargate/templates/story.md +1 -0
- package/templates/cleargate-planning/MANIFEST.json +71 -15
- package/dist/admin-api/index.cjs.map +0 -1
- package/dist/admin-api/index.js.map +0 -1
- package/dist/auth/factory.cjs.map +0 -1
- package/dist/auth/factory.js.map +0 -1
- package/dist/auth/require-token.cjs.map +0 -1
- package/dist/auth/require-token.js.map +0 -1
- package/dist/auth/token-store.cjs.map +0 -1
- package/dist/auth/token-store.js.map +0 -1
- package/dist/bootstrap-root-QKSA5V75.js.map +0 -1
- package/dist/chunk-5DI2Z3C2.js.map +0 -1
- package/dist/chunk-BTSZOEWC.js.map +0 -1
- package/dist/chunk-E3X7IE5E.js.map +0 -1
- package/dist/chunk-PDE37WFQ.js.map +0 -1
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/lib/ledger.cjs.map +0 -1
- package/dist/lib/ledger.js.map +0 -1
- package/dist/lib/lifecycle-reconcile.cjs.map +0 -1
- package/dist/lib/lifecycle-reconcile.js.map +0 -1
- package/dist/templates/cleargate-planning/.claude/agents/architect-reader.md +0 -61
- package/dist/templates/cleargate-planning/.claude/agents/architect-synth.md +0 -124
- package/dist/templates/cleargate-planning/.claude/agents/architect.md +0 -230
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +0 -108
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +0 -194
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +0 -261
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-query.md +0 -143
- package/dist/templates/cleargate-planning/.claude/agents/developer.md +0 -185
- package/dist/templates/cleargate-planning/.claude/agents/devops.md +0 -257
- package/dist/templates/cleargate-planning/.claude/agents/qa.md +0 -171
- package/dist/templates/cleargate-planning/.claude/agents/reporter.md +0 -274
- package/dist/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +0 -209
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +0 -33
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +0 -58
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit.sh +0 -19
- package/dist/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +0 -162
- package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-autonomy.sh +0 -58
- package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +0 -148
- package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +0 -75
- package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +0 -43
- package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +0 -590
- package/dist/templates/cleargate-planning/.claude/settings.json +0 -68
- package/dist/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +0 -102
- package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +0 -742
- package/dist/templates/cleargate-planning/.cleargate/FLASHCARD.md +0 -7
- package/dist/templates/cleargate-planning/.cleargate/config.example.yml +0 -67
- package/dist/templates/cleargate-planning/.cleargate/config.yml +0 -18
- package/dist/templates/cleargate-planning/.cleargate/delivery/archive/.gitkeep +0 -0
- package/dist/templates/cleargate-planning/.cleargate/delivery/pending-sync/.gitkeep +0 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +0 -551
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +0 -878
- package/dist/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +0 -160
- package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +0 -213
- package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +0 -71
- package/dist/templates/cleargate-planning/.cleargate/scripts/_migrate-schema-v3.mjs +0 -120
- package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +0 -265
- package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +0 -1012
- package/dist/templates/cleargate-planning/.cleargate/scripts/collision_surface.sh +0 -114
- package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +0 -62
- package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +0 -219
- package/dist/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +0 -320
- package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +0 -15
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +0 -38
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +0 -240
- package/dist/templates/cleargate-planning/.cleargate/scripts/launch_wave.mjs +0 -341
- package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +0 -54
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +0 -206
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +0 -371
- package/dist/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +0 -280
- package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +0 -378
- package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +0 -888
- package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +0 -209
- package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +0 -71
- package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +0 -127
- package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +0 -717
- package/dist/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +0 -27
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +0 -261
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +0 -210
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +0 -190
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +0 -482
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +0 -327
- package/dist/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +0 -261
- package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +0 -246
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +0 -111
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +0 -184
- package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +0 -172
- package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +0 -126
- package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +0 -130
- package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +0 -137
- package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +0 -166
- package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +0 -111
- package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +0 -122
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +0 -50
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +0 -224
- package/dist/templates/cleargate-planning/.cleargate/templates/story.md +0 -213
- package/dist/templates/cleargate-planning/CLAUDE.md +0 -66
- package/dist/templates/cleargate-planning/MANIFEST.json +0 -503
- package/dist/templates/synthesis/active-sprint.md +0 -30
- package/dist/templates/synthesis/open-gates.md +0 -38
- package/dist/templates/synthesis/product-state.md +0 -31
- package/dist/templates/synthesis/roadmap.md +0 -63
- package/dist/whoami-EANGN46Z.js.map +0 -1
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# collision_surface.sh — STORY-033-03 (EPIC-033)
|
|
3
|
-
#
|
|
4
|
-
# Standalone fork of parse_surface_paths() from file_surface_diff.sh:158-189.
|
|
5
|
-
# FIXES the single-column bug: the original reads only cols[2]; this fork scans
|
|
6
|
-
# ALL columns to detect file paths regardless of which column they appear in.
|
|
7
|
-
#
|
|
8
|
-
# Usage: collision_surface.sh <story-file>
|
|
9
|
-
# Emits one file path per line, deduped, backticks stripped, comma-split cells split.
|
|
10
|
-
# Exit 0 always.
|
|
11
|
-
#
|
|
12
|
-
# FAIL-SAFE CONTRACT (BUG-033): when ZERO paths are parseable from the §3.1 table
|
|
13
|
-
# (no table, prose-only table, or only slash-free/extension-less tokens), this
|
|
14
|
-
# script emits EMPTY stdout AND a `[collision_surface] WARN:` line on STDERR.
|
|
15
|
-
# The wave-compatibility predicate (architect-synth) MUST treat an empty surface
|
|
16
|
-
# as "cannot prove disjointness" → fail-safe-serialize, NEVER as
|
|
17
|
-
# "empty ∩ empty = ∅ ⇒ disjoint". Empty stdout is a SERIALIZE signal, not a
|
|
18
|
-
# parallelize signal. Over-serialization is the safe failure direction.
|
|
19
|
-
#
|
|
20
|
-
# Used by architect-synth to compute the five-clause wave-compatibility predicate.
|
|
21
|
-
# Do NOT modify file_surface_diff.sh — this is a deliberate standalone fork.
|
|
22
|
-
|
|
23
|
-
set -euo pipefail
|
|
24
|
-
|
|
25
|
-
STORY_FILE="${1:-}"
|
|
26
|
-
|
|
27
|
-
if [[ -z "${STORY_FILE}" ]]; then
|
|
28
|
-
echo "[collision_surface] ERROR: No story file argument provided." >&2
|
|
29
|
-
echo "[collision_surface] Usage: $0 <story-file>" >&2
|
|
30
|
-
exit 1
|
|
31
|
-
fi
|
|
32
|
-
|
|
33
|
-
if [[ ! -f "${STORY_FILE}" ]]; then
|
|
34
|
-
echo "[collision_surface] ERROR: Story file not found: ${STORY_FILE}" >&2
|
|
35
|
-
exit 1
|
|
36
|
-
fi
|
|
37
|
-
|
|
38
|
-
# ---- Parse §3.1 file surface table (multi-column fix) -----------------------
|
|
39
|
-
#
|
|
40
|
-
# CHANGE vs file_surface_diff.sh:158-189:
|
|
41
|
-
# Original: val=cols[2] — reads only the second column (the "Value" column).
|
|
42
|
-
# Fork: iterate cols[1..n] — checks every column for path-like tokens.
|
|
43
|
-
#
|
|
44
|
-
# Non-path skip list (tightened to avoid false matches with col-1 labels):
|
|
45
|
-
# "Yes", "No", "Yes/No", "N/A", and any prefix of "Yes/No —"
|
|
46
|
-
#
|
|
47
|
-
# Path-shape guard (looks_like_path): a token is a path IFF it contains "/" OR ends
|
|
48
|
-
# in a known file extension. This catches both slash-bearing paths (a/b.ts) AND
|
|
49
|
-
# bare filenames the prior slash-only guard missed (package.json, schema.sql), while
|
|
50
|
-
# still rejecting label cells like "Primary File (new)" or prose like "Some details.".
|
|
51
|
-
# BUG-033: the guard now matches the documented contract — the prior code was
|
|
52
|
-
# slash-only despite the comment claiming "/ OR known extension".
|
|
53
|
-
#
|
|
54
|
-
# Over-serialization is the safe failure direction — a token we cannot classify as a
|
|
55
|
-
# path is dropped, so the story trends toward an empty surface → fail-safe-serialize.
|
|
56
|
-
|
|
57
|
-
parse_surface_paths() {
|
|
58
|
-
local story_file="$1"
|
|
59
|
-
awk '
|
|
60
|
-
# A token is a path IFF it contains "/" OR ends in a known file extension.
|
|
61
|
-
function looks_like_path(s) {
|
|
62
|
-
return (s ~ /\//) || \
|
|
63
|
-
(s ~ /\.(ts|tsx|cts|mts|js|jsx|cjs|mjs|json|jsonc|sh|bash|md|sql|svelte|vue|css|scss|sass|less|ya?ml|toml|ini|env|txt|prisma|html|htm|py|rs|go|rb|java|kt)$/)
|
|
64
|
-
}
|
|
65
|
-
/^### 3\.1/ { in_section=1; next }
|
|
66
|
-
in_section && /^### / { in_section=0; next }
|
|
67
|
-
in_section && /^\|/ {
|
|
68
|
-
line=$0
|
|
69
|
-
gsub(/^\||\|$/, "", line)
|
|
70
|
-
n=split(line, cols, "|")
|
|
71
|
-
# Scan ALL columns (fix: was only cols[2])
|
|
72
|
-
for (c=1; c<=n; c++) {
|
|
73
|
-
val=cols[c]
|
|
74
|
-
# Trim whitespace
|
|
75
|
-
gsub(/^[[:space:]]+|[[:space:]]+$/, "", val)
|
|
76
|
-
# Strip backticks
|
|
77
|
-
gsub(/`/, "", val)
|
|
78
|
-
# Skip known non-path cells (exact and prefix matches)
|
|
79
|
-
if (val == "Yes") continue
|
|
80
|
-
if (val == "No") continue
|
|
81
|
-
if (val == "Yes/No") continue
|
|
82
|
-
if (val == "N/A") continue
|
|
83
|
-
if (val == "Item") continue
|
|
84
|
-
if (val == "Value") continue
|
|
85
|
-
if (val == "") continue
|
|
86
|
-
# Skip cells starting with "Yes/No —" (partial answers)
|
|
87
|
-
if (substr(val, 1, 8) == "Yes/No -") continue
|
|
88
|
-
# Handle multiple paths separated by ", "; guard each split token.
|
|
89
|
-
npaths=split(val, paths, ", ")
|
|
90
|
-
for (i=1; i<=npaths; i++) {
|
|
91
|
-
p=paths[i]
|
|
92
|
-
gsub(/^[[:space:]]+|[[:space:]]+$/, "", p)
|
|
93
|
-
if (p != "" && looks_like_path(p)) print p
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
' "${story_file}"
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
# Collect paths and deduplicate (portable bash 3.2 compat — no mapfile, no declare -A on macOS)
|
|
101
|
-
# FLASHCARD #bash #macos: no mapfile on macOS bash 3.2 — use while-read loop
|
|
102
|
-
# FLASHCARD #bash #macos: no associative arrays (declare -A) on macOS bash 3.2 — use awk for dedup
|
|
103
|
-
SURFACE="$(parse_surface_paths "${STORY_FILE}" | awk '!seen[$0]++')"
|
|
104
|
-
|
|
105
|
-
if [[ -z "${SURFACE}" ]]; then
|
|
106
|
-
# BUG-033 fail-safe signal. ZERO parseable paths = the disjointness predicate CANNOT
|
|
107
|
-
# prove this story is collision-free, so it must NOT be co-waved. stdout stays empty
|
|
108
|
-
# (the caller reads "no proven surface"); the predicate fail-safe-serializes on empty.
|
|
109
|
-
echo "[collision_surface] WARN: no parseable file surface in ${STORY_FILE} — downstream MUST fail-safe-serialize (empty surface is NOT 'disjoint')." >&2
|
|
110
|
-
exit 0
|
|
111
|
-
fi
|
|
112
|
-
|
|
113
|
-
printf '%s\n' "${SURFACE}"
|
|
114
|
-
exit 0
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ClearGate Execution Phase — Constants
|
|
3
|
-
*
|
|
4
|
-
* state.json v3 Schema (STORY-070-01: execution_mode retired — single always-enforced behavior):
|
|
5
|
-
*
|
|
6
|
-
* {
|
|
7
|
-
* "schema_version": 3, // integer, mandatory
|
|
8
|
-
* "sprint_id": "S-NN", // string
|
|
9
|
-
* "sprint_status": "Active", // string
|
|
10
|
-
* "stories": {
|
|
11
|
-
* "STORY-NNN-NN": {
|
|
12
|
-
* "state": "Ready to Bounce", // one of VALID_STATES
|
|
13
|
-
* "qa_bounces": 0, // integer 0..BOUNCE_CAP
|
|
14
|
-
* "arch_bounces": 0, // integer 0..BOUNCE_CAP
|
|
15
|
-
* "worktree": null, // string|null — path to worktree checkout
|
|
16
|
-
* "updated_at": "<ISO-8601>", // string
|
|
17
|
-
* "notes": "", // string
|
|
18
|
-
* "lane": "standard", // default "standard"
|
|
19
|
-
* "lane_assigned_by": "architect" | "human-override" | "migration-default",
|
|
20
|
-
* "lane_demoted_at": "<ISO-8601>" | null,
|
|
21
|
-
* "lane_demotion_reason": string | null
|
|
22
|
-
* }
|
|
23
|
-
* },
|
|
24
|
-
* "last_action": "<string>", // human-readable last operation
|
|
25
|
-
* "updated_at": "<ISO-8601>" // string
|
|
26
|
-
* }
|
|
27
|
-
*
|
|
28
|
-
* Break-glass env var: CLEARGATE_ADVISORY=1 downgrades gate failures to warnings.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
export const SCHEMA_VERSION = 3;
|
|
32
|
-
|
|
33
|
-
export const BOUNCE_CAP = 3;
|
|
34
|
-
|
|
35
|
-
export const VALID_STATES = [
|
|
36
|
-
'Ready to Bounce',
|
|
37
|
-
'Bouncing',
|
|
38
|
-
'QA Passed',
|
|
39
|
-
'Architect Passed',
|
|
40
|
-
'Sprint Review',
|
|
41
|
-
'Done',
|
|
42
|
-
'Escalated',
|
|
43
|
-
'Parking Lot',
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
export const TERMINAL_STATES = ['Done', 'Escalated', 'Parking Lot'];
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Canonical state-machine transitions table.
|
|
50
|
-
* Key: current state. Value: array of allowed next states.
|
|
51
|
-
* Terminal states have empty arrays (no transitions out).
|
|
52
|
-
*/
|
|
53
|
-
export const STATE_TRANSITIONS = {
|
|
54
|
-
'Ready to Bounce': ['Bouncing', 'Parking Lot'],
|
|
55
|
-
'Bouncing': ['QA Passed', 'Ready to Bounce', 'Escalated', 'Parking Lot'],
|
|
56
|
-
'QA Passed': ['Architect Passed', 'Ready to Bounce', 'Escalated', 'Parking Lot'],
|
|
57
|
-
'Architect Passed': ['Sprint Review', 'Ready to Bounce', 'Escalated', 'Parking Lot'],
|
|
58
|
-
'Sprint Review': ['Done', 'Ready to Bounce', 'Escalated', 'Parking Lot'],
|
|
59
|
-
'Done': [],
|
|
60
|
-
'Escalated': [],
|
|
61
|
-
'Parking Lot': [],
|
|
62
|
-
};
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* dedupe_frontmatter.mjs — BUG-025
|
|
4
|
-
*
|
|
5
|
-
* One-shot corpus dedupe pass: scan every .md file under
|
|
6
|
-
* .cleargate/delivery/pending-sync/ and .cleargate/delivery/archive/.
|
|
7
|
-
* For any file whose YAML frontmatter contains duplicate top-level keys,
|
|
8
|
-
* keep the LAST occurrence of each key (closest to the body — this is what
|
|
9
|
-
* the stamp hook writes most recently) and rewrite the file.
|
|
10
|
-
*
|
|
11
|
-
* Idempotent: re-running produces zero diff when no duplicates remain.
|
|
12
|
-
*
|
|
13
|
-
* Usage:
|
|
14
|
-
* node .cleargate/scripts/dedupe_frontmatter.mjs [--dry-run] [<dir>]
|
|
15
|
-
*
|
|
16
|
-
* --dry-run Print which files would be rewritten without writing.
|
|
17
|
-
* <dir> Walk only this directory instead of the canonical corpus dirs.
|
|
18
|
-
* Used by integration tests targeting a tmpdir.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import * as fs from 'node:fs';
|
|
22
|
-
import * as path from 'node:path';
|
|
23
|
-
import * as url from 'node:url';
|
|
24
|
-
|
|
25
|
-
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
26
|
-
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
27
|
-
|
|
28
|
-
const args = process.argv.slice(2);
|
|
29
|
-
const DRY_RUN = args.includes('--dry-run');
|
|
30
|
-
const DIR_ARG = args.find((a) => !a.startsWith('--'));
|
|
31
|
-
|
|
32
|
-
const PENDING_SYNC = path.join(REPO_ROOT, '.cleargate', 'delivery', 'pending-sync');
|
|
33
|
-
const ARCHIVE = path.join(REPO_ROOT, '.cleargate', 'delivery', 'archive');
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Collect all .md files in a flat directory (non-recursive — delivery dirs are flat).
|
|
37
|
-
*/
|
|
38
|
-
function collectMd(dir) {
|
|
39
|
-
let entries;
|
|
40
|
-
try {
|
|
41
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
42
|
-
} catch {
|
|
43
|
-
return [];
|
|
44
|
-
}
|
|
45
|
-
return entries
|
|
46
|
-
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
47
|
-
.map((e) => path.join(dir, e.name));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Parse a raw markdown string into:
|
|
52
|
-
* fmLines — the lines between the opening and closing `---` delimiters
|
|
53
|
-
* closeIdx — index of the closing `---` line (in the full `lines` array)
|
|
54
|
-
* lines — all lines of the file
|
|
55
|
-
*
|
|
56
|
-
* Returns null if the file has no valid frontmatter.
|
|
57
|
-
*/
|
|
58
|
-
function parseFmLines(raw) {
|
|
59
|
-
const lines = raw.split('\n');
|
|
60
|
-
if (lines[0] !== '---') return null;
|
|
61
|
-
let closeIdx = -1;
|
|
62
|
-
for (let i = 1; i < lines.length; i++) {
|
|
63
|
-
if (lines[i] === '---') {
|
|
64
|
-
closeIdx = i;
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (closeIdx === -1) return null;
|
|
69
|
-
return { lines, closeIdx, fmLines: lines.slice(1, closeIdx) };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Given the frontmatter lines, detect duplicate top-level keys.
|
|
74
|
-
* Returns a Map from key → array of line-indices (within fmLines) where it appears.
|
|
75
|
-
* Only entries with ≥2 occurrences indicate duplicates.
|
|
76
|
-
*
|
|
77
|
-
* A "top-level key" is a line that starts with a non-space character followed by
|
|
78
|
-
* `:<space>` or `:<end-of-line>`. Multi-line values (YAML scalars, blocks) that
|
|
79
|
-
* contain `:` on continuation lines are NOT top-level keys (they start with space/tab).
|
|
80
|
-
*/
|
|
81
|
-
function findDuplicateKeys(fmLines) {
|
|
82
|
-
/** @type {Map<string, number[]>} */
|
|
83
|
-
const keyMap = new Map();
|
|
84
|
-
for (let i = 0; i < fmLines.length; i++) {
|
|
85
|
-
const line = fmLines[i];
|
|
86
|
-
// Top-level key: starts at column 0, has `:` after a word character sequence
|
|
87
|
-
const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*):/);
|
|
88
|
-
if (m) {
|
|
89
|
-
const key = m[1];
|
|
90
|
-
if (!keyMap.has(key)) {
|
|
91
|
-
keyMap.set(key, []);
|
|
92
|
-
}
|
|
93
|
-
keyMap.get(key).push(i);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// Return only keys with duplicates
|
|
97
|
-
/** @type {Map<string, number[]>} */
|
|
98
|
-
const dupes = new Map();
|
|
99
|
-
for (const [k, indices] of keyMap) {
|
|
100
|
-
if (indices.length > 1) {
|
|
101
|
-
dupes.set(k, indices);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return dupes;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Deduplicate frontmatter lines: for each duplicate key, keep the LAST occurrence
|
|
109
|
-
* and discard all earlier ones (including their potential multi-line values).
|
|
110
|
-
*
|
|
111
|
-
* Multi-line value detection: lines that follow a key line and start with
|
|
112
|
-
* whitespace (` ` or `\t`) belong to the preceding key's value.
|
|
113
|
-
*
|
|
114
|
-
* Returns the deduplicated fmLines array (may be the same reference if no changes).
|
|
115
|
-
*/
|
|
116
|
-
function dedupeLines(fmLines, dupes) {
|
|
117
|
-
if (dupes.size === 0) return fmLines;
|
|
118
|
-
|
|
119
|
-
// Build a set of fmLine indices to DROP (all but the last occurrence of each dup key,
|
|
120
|
-
// including their continuation lines).
|
|
121
|
-
/** @type {Set<number>} */
|
|
122
|
-
const dropSet = new Set();
|
|
123
|
-
|
|
124
|
-
for (const [, indices] of dupes) {
|
|
125
|
-
// Keep last occurrence; drop all earlier ones (+ their continuations)
|
|
126
|
-
const toRemove = indices.slice(0, -1); // all but last
|
|
127
|
-
for (const startIdx of toRemove) {
|
|
128
|
-
dropSet.add(startIdx);
|
|
129
|
-
// Mark continuation lines (indent-starting lines following a key line)
|
|
130
|
-
let j = startIdx + 1;
|
|
131
|
-
while (j < fmLines.length && /^[ \t]/.test(fmLines[j])) {
|
|
132
|
-
dropSet.add(j);
|
|
133
|
-
j++;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return fmLines.filter((_, i) => !dropSet.has(i));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Atomic write: write to a .tmp file then rename over the target.
|
|
143
|
-
*/
|
|
144
|
-
function writeAtomic(filePath, content) {
|
|
145
|
-
const tmpPath = `${filePath}.tmp.${Date.now()}`;
|
|
146
|
-
fs.writeFileSync(tmpPath, content, 'utf8');
|
|
147
|
-
fs.renameSync(tmpPath, filePath);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
151
|
-
|
|
152
|
-
const files = DIR_ARG
|
|
153
|
-
? collectMd(path.resolve(DIR_ARG))
|
|
154
|
-
: [...collectMd(PENDING_SYNC), ...collectMd(ARCHIVE)];
|
|
155
|
-
|
|
156
|
-
let rewritten = 0;
|
|
157
|
-
let skipped = 0;
|
|
158
|
-
let errors = 0;
|
|
159
|
-
|
|
160
|
-
for (const filePath of files) {
|
|
161
|
-
let raw;
|
|
162
|
-
try {
|
|
163
|
-
raw = fs.readFileSync(filePath, 'utf8');
|
|
164
|
-
} catch (e) {
|
|
165
|
-
console.error(`error reading ${filePath}: ${e.message}`);
|
|
166
|
-
errors++;
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const parsed = parseFmLines(raw);
|
|
171
|
-
if (!parsed) {
|
|
172
|
-
skipped++;
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const { lines, closeIdx, fmLines } = parsed;
|
|
177
|
-
const dupes = findDuplicateKeys(fmLines);
|
|
178
|
-
|
|
179
|
-
if (dupes.size === 0) {
|
|
180
|
-
skipped++;
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const relPath = path.relative(REPO_ROOT, filePath);
|
|
185
|
-
const dupeSummary = Array.from(dupes.keys()).join(', ');
|
|
186
|
-
|
|
187
|
-
if (DRY_RUN) {
|
|
188
|
-
console.log(`would-rewrite: ${relPath} (duplicate keys: ${dupeSummary})`);
|
|
189
|
-
rewritten++;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Build the new file content: deduplicated frontmatter + rest unchanged
|
|
194
|
-
const cleanedFmLines = dedupeLines(fmLines, dupes);
|
|
195
|
-
const newLines = [
|
|
196
|
-
lines[0], // opening ---
|
|
197
|
-
...cleanedFmLines,
|
|
198
|
-
lines[closeIdx], // closing ---
|
|
199
|
-
...lines.slice(closeIdx + 1),
|
|
200
|
-
];
|
|
201
|
-
const newContent = newLines.join('\n');
|
|
202
|
-
|
|
203
|
-
// Verify the result is actually different (guard against no-op edge cases)
|
|
204
|
-
if (newContent === raw) {
|
|
205
|
-
skipped++;
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
writeAtomic(filePath, newContent);
|
|
210
|
-
console.log(`rewritten: ${relPath} (removed duplicate keys: ${dupeSummary})`);
|
|
211
|
-
rewritten++;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
console.log(
|
|
215
|
-
`\nDedupe complete: ${rewritten} ${DRY_RUN ? 'would-rewrite' : 'rewritten'}, ${skipped} skipped, ${errors} errors.`,
|
|
216
|
-
);
|
|
217
|
-
if (errors > 0) {
|
|
218
|
-
process.exit(1);
|
|
219
|
-
}
|