@latysh-development/monolith 0.1.0-alpha.2 → 0.1.0-alpha.4

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.
Files changed (90) hide show
  1. package/README.md +24 -1
  2. package/dist/brief/cli.d.ts.map +1 -1
  3. package/dist/brief/cli.js +8 -2
  4. package/dist/brief/collect.d.ts.map +1 -1
  5. package/dist/brief/collect.js +17 -5
  6. package/dist/brief/freshness.d.ts +50 -7
  7. package/dist/brief/freshness.d.ts.map +1 -1
  8. package/dist/brief/freshness.js +89 -17
  9. package/dist/brief/git-signals.d.ts +32 -5
  10. package/dist/brief/git-signals.d.ts.map +1 -1
  11. package/dist/brief/git-signals.js +169 -39
  12. package/dist/brief/knowledge.d.ts +62 -0
  13. package/dist/brief/knowledge.d.ts.map +1 -0
  14. package/dist/brief/knowledge.js +123 -0
  15. package/dist/brief/markdown-context.d.ts +22 -0
  16. package/dist/brief/markdown-context.d.ts.map +1 -0
  17. package/dist/brief/markdown-context.js +66 -0
  18. package/dist/brief/reconcile.d.ts +7 -1
  19. package/dist/brief/reconcile.d.ts.map +1 -1
  20. package/dist/brief/reconcile.js +42 -2
  21. package/dist/brief/story.d.ts +2 -10
  22. package/dist/brief/story.d.ts.map +1 -1
  23. package/dist/brief/story.js +21 -7
  24. package/dist/cli/index.js +1 -1
  25. package/dist/cli/style.d.ts +26 -0
  26. package/dist/cli/style.d.ts.map +1 -0
  27. package/dist/cli/style.js +76 -0
  28. package/dist/cli/ui.d.ts.map +1 -1
  29. package/dist/cli/ui.js +33 -6
  30. package/dist/coordinatorium/cli.d.ts.map +1 -1
  31. package/dist/coordinatorium/cli.js +13 -0
  32. package/dist/finalize/cli.d.ts.map +1 -1
  33. package/dist/finalize/cli.js +30 -13
  34. package/dist/finalize/consistency.d.ts +51 -0
  35. package/dist/finalize/consistency.d.ts.map +1 -0
  36. package/dist/finalize/consistency.js +57 -0
  37. package/dist/finalize/core.d.ts +28 -0
  38. package/dist/finalize/core.d.ts.map +1 -1
  39. package/dist/finalize/core.js +186 -41
  40. package/dist/ideas/cli.d.ts +1 -0
  41. package/dist/ideas/cli.d.ts.map +1 -1
  42. package/dist/ideas/cli.js +14 -3
  43. package/dist/ideas/format.d.ts +31 -1
  44. package/dist/ideas/format.d.ts.map +1 -1
  45. package/dist/ideas/format.js +109 -1
  46. package/dist/ideas/store.d.ts +2 -0
  47. package/dist/ideas/store.d.ts.map +1 -1
  48. package/dist/ideas/store.js +4 -1
  49. package/dist/ideas/types.d.ts +10 -0
  50. package/dist/ideas/types.d.ts.map +1 -1
  51. package/dist/ideas/types.js +3 -0
  52. package/dist/readmodel/dashboard.d.ts.map +1 -1
  53. package/dist/readmodel/dashboard.js +19 -1
  54. package/dist/readmodel/index.d.ts +2 -1
  55. package/dist/readmodel/index.d.ts.map +1 -1
  56. package/dist/readmodel/index.js +1 -0
  57. package/dist/readmodel/roadmap.d.ts +34 -0
  58. package/dist/readmodel/roadmap.d.ts.map +1 -0
  59. package/dist/readmodel/roadmap.js +254 -0
  60. package/dist/readmodel/types.d.ts +88 -0
  61. package/dist/readmodel/types.d.ts.map +1 -1
  62. package/dist/root/safe-write.d.ts +15 -4
  63. package/dist/root/safe-write.d.ts.map +1 -1
  64. package/dist/root/safe-write.js +264 -50
  65. package/dist/ui/apple-touch-icon.png +0 -0
  66. package/dist/ui/assets/index-BJKxUDPD.css +1 -0
  67. package/dist/ui/assets/index-ioC9DPHQ.js +28 -0
  68. package/dist/ui/assets/monolith-mark-Ct0ammrU.webp +0 -0
  69. package/dist/ui/favicon-16x16.png +0 -0
  70. package/dist/ui/favicon-32x32.png +0 -0
  71. package/dist/ui/favicon.ico +0 -0
  72. package/dist/ui/index.html +6 -2
  73. package/dist/update/cli.d.ts +1 -1
  74. package/dist/update/cli.d.ts.map +1 -1
  75. package/dist/update/cli.js +10 -3
  76. package/dist/update/index.d.ts +9 -0
  77. package/dist/update/index.d.ts.map +1 -1
  78. package/dist/update/index.js +50 -1
  79. package/dist/update/legacy-skills.d.ts +15 -0
  80. package/dist/update/legacy-skills.d.ts.map +1 -0
  81. package/dist/update/legacy-skills.js +20 -0
  82. package/dist/validate/index.d.ts.map +1 -1
  83. package/dist/validate/index.js +4 -3
  84. package/dist/version.d.ts +1 -1
  85. package/dist/version.js +1 -1
  86. package/package.json +5 -2
  87. package/templates/default-instance/monolith/context/MONOLITH-CHAT-CONTEXT.md +183 -0
  88. package/templates/default-instance/monolith/skills/finalize-story/SKILL.md +19 -3
  89. package/dist/ui/assets/index-C4NhMaq7.js +0 -28
  90. package/dist/ui/assets/index-DMZ7ZvZY.css +0 -1
package/README.md CHANGED
@@ -101,6 +101,29 @@ monolith ui # Mission Control at http://127.0.0.1:4173 ; Ctrl+C to stop
101
101
  instance, and it does not open a browser — open the printed address yourself.
102
102
  All commands: [CLI reference](./docs/reference/cli.md).
103
103
 
104
+ ### 🧠 One More Thing: Give Your AI Chat the Context!
105
+
106
+ MONOLITH is installed. Now make sure your AI assistant knows how to use it.
107
+
108
+ Open `.monolith/context/MONOLITH-CHAT-CONTEXT.md` and copy its contents into your ChatGPT Project instructions, Claude conversation, or another AI workspace you use to coordinate development.
109
+
110
+ It explains MONOLITH's capabilities, project memory, workflows, and limitations.
111
+
112
+ **Your coding agent gets the protocols. Your working chat gets the context.**
113
+
114
+ `monolith init` creates this file. Print it from the project root (the directory where you ran `monolith init`):
115
+
116
+ ```bash
117
+ cat .monolith/context/MONOLITH-CHAT-CONTEXT.md
118
+ ```
119
+
120
+ If `monolithRoot` in `monolith.config.json` is not `.monolith`, use that directory instead.
121
+ Projects initialized with an earlier MONOLITH version do not get this file from
122
+ `monolith update`; copy it from the template in this repository (replace
123
+ `{{MONOLITH_ROOT}}` with `.monolith`, otherwise `monolith validate` reports an
124
+ unreplaced placeholder). You can also read the template before installing:
125
+ [`templates/default-instance/monolith/context/MONOLITH-CHAT-CONTEXT.md`](./templates/default-instance/monolith/context/MONOLITH-CHAT-CONTEXT.md).
126
+
104
127
  ## Updating an Existing Project
105
128
 
106
129
  If a project was initialised with an earlier MONOLITH version, bring it up to the current agent integration (agent entry point and Idea Capture) with:
@@ -160,7 +183,7 @@ listing — not an official partnership, endorsement or certification.
160
183
 
161
184
  ## Project Status
162
185
 
163
- **Alpha** (`0.1.0-alpha.2`). The API and CLI are experimental and may change
186
+ **Alpha** (`0.1.0-alpha.4`). The API and CLI are experimental and may change
164
187
  without notice. Published to npm under the alpha dist-tag. Durable State and the Coordinatorium
165
188
  (`monolith durable`, `monolith mission`) are experimental and are not part of
166
189
  the Quick Start.
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/brief/cli.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;GAcG;AAMH,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,IAAI,CAAC;IACT,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,iGAAiG;IACjG,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,KAAK,CAAC;IACV,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,YAAY,CAAC;AAIzD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CA6C9D;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BvF"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/brief/cli.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;GAcG;AAOH,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,IAAI,CAAC;IACT,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,iGAAiG;IACjG,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,KAAK,CAAC;IACV,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,YAAY,CAAC;AAIzD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CA6C9D;AAQD,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BvF"}
package/dist/brief/cli.js CHANGED
@@ -14,6 +14,7 @@
14
14
  * -> 0, whatever the Freshness; usage error -> 1). It does not combine with
15
15
  * `--strict`, whose meaning (Completeness) belongs to the brief.
16
16
  */
17
+ import { colorEnabled, paint, tintStatuses } from '../cli/style.js';
17
18
  import { collect } from './collect.js';
18
19
  import { render, DEFAULT_MAX_CHARS } from './render.js';
19
20
  import { reconcile, renderReconciliation } from './reconcile.js';
@@ -60,6 +61,11 @@ export function parseBriefArgs(args) {
60
61
  }
61
62
  return reconcileMode ? { ok: true, maxChars, strict, reconcile: true } : { ok: true, maxChars, strict };
62
63
  }
64
+ /** Interactive terminals only: tints the Freshness / Completeness verdict words; plain text otherwise (byte for byte). */
65
+ function tintBrief(text) {
66
+ const on = colorEnabled();
67
+ return tintStatuses(on ? text.replace(/^MONOLITH [A-Z ]+$/m, (h) => paint(h, 'head', true)) : text, on, ['Freshness', 'Completeness']);
68
+ }
63
69
  export async function runBrief(args, targetDir = process.cwd()) {
64
70
  const parsed = parseBriefArgs(args);
65
71
  if (!parsed.ok) {
@@ -70,11 +76,11 @@ export async function runBrief(args, targetDir = process.cwd()) {
70
76
  if (!model.freshness || !model.freshnessEvidence) {
71
77
  throw new Error('monolith brief --reconcile: no freshness evidence could be collected.');
72
78
  }
73
- process.stdout.write(renderReconciliation(reconcile(model.freshnessEvidence, model.freshness), parsed.maxChars).text);
79
+ process.stdout.write(tintBrief(renderReconciliation(reconcile(model.freshnessEvidence, model.freshness), parsed.maxChars).text));
74
80
  process.exit(0);
75
81
  }
76
82
  const { text, completeness } = render(model, parsed.maxChars);
77
- process.stdout.write(text);
83
+ process.stdout.write(tintBrief(text));
78
84
  if (completeness === 'INCOMPLETE') {
79
85
  console.error('monolith brief: result is INCOMPLETE — see the Diagnostics section above.');
80
86
  }
@@ -1 +1 @@
1
- {"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../../src/brief/collect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAwBH,OAAO,KAAK,EACV,UAAU,EAOX,MAAM,YAAY,CAAC;AAiBpB,kFAAkF;AAClF,eAAO,MAAM,wBAAwB,QAAa,CAAC;AA8TnD,wBAAsB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAqEpE"}
1
+ {"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../../src/brief/collect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAyBH,OAAO,KAAK,EACV,UAAU,EAOX,MAAM,YAAY,CAAC;AAmBpB,kFAAkF;AAClF,eAAO,MAAM,wBAAwB,QAAa,CAAC;AAwUnD,wBAAsB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAqEpE"}
@@ -16,11 +16,13 @@ import { readBriefConfig } from './config.js';
16
16
  import { resolveMonolithRootValue, checkContainedFile, readBoundedFileStable, isReadSecurityViolation, findWriteArtifacts, describeWriteArtifacts, } from '../root/index.js';
17
17
  import { discoverDocs } from './docs.js';
18
18
  import { extractCurrentStory } from './story.js';
19
+ import { maskNonContent } from './markdown-context.js';
19
20
  import { selectActiveMissions, selectRecentFinishedMissions, selectLatestCheckpoint, hasCheckpointOrderMismatch, } from './selectors.js';
20
21
  import { stateFilePath, validateState, createInitialState, MAX_STATE_BYTES } from '../coordinatorium/index.js';
21
22
  import { ACTIVE_MD_REL, PARKED_IDEAS_LIMIT, PROTOCOLS_DIR, ROADMAP_INDEX_REL, STORY_INDEX_REL, HANDOFF_REL } from './constants.js';
22
- import { evaluateFreshness, parseActiveStoryClaim, parseRoadmapActiveStory, parseRoadmapStatuses, parseStoryIndexStatuses, } from './freshness.js';
23
- import { readGitSignals } from './git-signals.js';
23
+ import { evaluateFreshness, extractWorkItemIds, parseActiveStoryClaim, parseRoadmapActiveStory, parseRoadmapStatuses, parseStoryIndexStatuses, } from './freshness.js';
24
+ import { readBlobAt, readGitSignals } from './git-signals.js';
25
+ import { attributeKnowledge } from './knowledge.js';
24
26
  import { ideaStore, listIdeas } from '../ideas/index.js';
25
27
  import { ideaNumber } from '../ideas/format.js';
26
28
  const RECENT_FINISHED_MISSIONS_LIMIT = 5;
@@ -202,7 +204,9 @@ function collectStory(targetDir, monolithRoot, root, diagnostics) {
202
204
  diagnostics.push({
203
205
  code: 'ACTIVE_MD_NO_CURRENT_STORY',
204
206
  severity: 'info',
205
- message: `${relPath} exists but has no "## Current Story" section.`,
207
+ message: extraction.ambiguous
208
+ ? `${relPath} has more than one "## Current Story" section outside code blocks and comments; the section is ambiguous and was not read.`
209
+ : `${relPath} exists but has no "## Current Story" section.`,
206
210
  });
207
211
  }
208
212
  return {
@@ -267,13 +271,21 @@ function collectFreshness(targetDir, monolithRoot, root, story) {
267
271
  const content = new Map(docs.map((rel) => [rel, readStateDocument(targetDir, monolithRoot, root, rel)]));
268
272
  const roadmap = content.get(ROADMAP_INDEX_REL) ?? null;
269
273
  const storyIndex = content.get(STORY_INDEX_REL) ?? null;
274
+ const git = readGitSignals(targetDir, stateFiles);
270
275
  const evidence = {
271
- git: readGitSignals(targetDir, stateFiles),
276
+ git,
277
+ knowledge: attributeKnowledge(git, {
278
+ monolithRoot,
279
+ stateFiles: new Set(stateFiles),
280
+ readBlob: (sha, rel) => readBlobAt(targetDir, sha, rel),
281
+ hasWorkItem: (subject) => extractWorkItemIds(subject).length > 0,
282
+ }),
272
283
  activeStory: story.fileExists && story.hasCurrentStorySection ? parseActiveStoryClaim(story.excerpt) : { kind: 'unknown' },
273
284
  roadmapActiveStory: roadmap === null ? null : parseRoadmapActiveStory(roadmap),
274
285
  roadmapStatuses: roadmap === null ? new Map() : parseRoadmapStatuses(roadmap),
275
286
  storyIndexStatuses: storyIndex === null ? new Map() : parseStoryIndexStatuses(storyIndex),
276
- recordedText: [...content.values()].filter((c) => c !== null).join('\n'),
287
+ // Ids shown only inside code fences or HTML comments are examples, not records.
288
+ recordedText: [...content.values()].filter((c) => c !== null).map(maskNonContent).join('\n'),
277
289
  stateFiles: new Set(stateFiles),
278
290
  };
279
291
  return { result: evaluateFreshness(evidence), evidence };
@@ -19,8 +19,10 @@
19
19
  * absence of an active story alone never produce STALE, and the
20
20
  * absence of an active story never means "project complete".
21
21
  */
22
+ import { maskNonContent } from './markdown-context.js';
23
+ import type { KnowledgeEvidence } from './knowledge.js';
22
24
  export type FreshnessStatus = 'FRESH' | 'STALE' | 'UNKNOWN';
23
- export type FreshnessReasonCode = 'STATE_CONTRADICTION' | 'UNRECORDED_WORK' | 'GIT_UNAVAILABLE' | 'STATE_NOT_IN_GIT' | 'GIT_HISTORY_TRUNCATED' | 'UNATTRIBUTED_ACTIVITY' | 'NO_ACTIVITY_SINCE_STATE_UPDATE' | 'ACTIVITY_RECORDED';
25
+ export type FreshnessReasonCode = 'STATE_CONTRADICTION' | 'UNRECORDED_WORK' | 'GIT_UNAVAILABLE' | 'STATE_NOT_IN_GIT' | 'GIT_HISTORY_TRUNCATED' | 'GIT_HISTORY_SHALLOW' | 'UNATTRIBUTED_ACTIVITY' | 'NO_ACTIVITY_SINCE_STATE_UPDATE' | 'ACTIVITY_RECORDED';
24
26
  export interface FreshnessReason {
25
27
  code: FreshnessReasonCode;
26
28
  message: string;
@@ -36,7 +38,7 @@ export interface GitCommitSignal {
36
38
  /** Paths changed by the commit, relative to the project directory. Empty for plain merges. */
37
39
  files: string[];
38
40
  }
39
- export type GitUnavailableReason = 'GIT_NOT_INSTALLED' | 'NOT_A_GIT_REPOSITORY' | 'GIT_FAILED';
41
+ export type GitUnavailableReason = 'GIT_NOT_INSTALLED' | 'NOT_A_GIT_REPOSITORY' | 'GIT_FAILED' | 'GIT_OUTPUT_INVALID';
40
42
  export type GitSignals = {
41
43
  status: 'unavailable';
42
44
  reason: GitUnavailableReason;
@@ -51,14 +53,22 @@ export type GitSignals = {
51
53
  commitsSince: GitCommitSignal[];
52
54
  /** True when git history was cut at the read limit — the picture is partial. */
53
55
  truncated: boolean;
56
+ /** True in a shallow clone: history before the boundary is absent, so "no later activity" proves nothing. */
57
+ shallow?: boolean;
54
58
  };
55
59
  export type StoryClaim = {
56
60
  kind: 'none';
57
61
  } | {
58
62
  kind: 'ids';
59
63
  ids: string[];
60
- } | {
64
+ }
65
+ /**
66
+ * `contradiction`: the section both says "no active story" and has a
67
+ * structural `**Story:**` field (PSC-004A) — the field value(s), verbatim.
68
+ */
69
+ | {
61
70
  kind: 'unknown';
71
+ contradiction?: string[];
62
72
  };
63
73
  export interface FreshnessEvidence {
64
74
  git: GitSignals;
@@ -73,6 +83,8 @@ export interface FreshnessEvidence {
73
83
  recordedText: string;
74
84
  /** Repo-relative-to-project paths that are state documents (their own commits are not "activity"). */
75
85
  stateFiles: ReadonlySet<string>;
86
+ /** KA-001: commits explained by registered independent knowledge. Absent = none (older callers). */
87
+ knowledge?: KnowledgeEvidence;
76
88
  }
77
89
  export interface FreshnessResult {
78
90
  status: FreshnessStatus;
@@ -92,13 +104,44 @@ export interface FreshnessResult {
92
104
  }
93
105
  export declare function extractWorkItemIds(text: string): string[];
94
106
  export declare function mentionsId(text: string, id: string): boolean;
95
- /** Interprets the `## Current Story` excerpt of active.md. */
107
+ export { maskNonContent };
108
+ /**
109
+ * Interprets the `## Current Story` excerpt of active.md.
110
+ *
111
+ * Identity comes from the structural `**Story:** <id> …` field only (first
112
+ * token = id), exactly as DOCSYNC reads it. Ids in titles, prose, code fences
113
+ * or comments are never identity. Zero usable, several or malformed fields →
114
+ * `unknown` (never a guess, never STALE on its own).
115
+ *
116
+ * Compatibility: a historical section with no `**Story:**` field at all is
117
+ * still read when its first line is a *bold declaration* of exactly one
118
+ * work-item id — `**WIDGET-001 — title**`, `**WIDGET-001**` or
119
+ * `**WIDGET-001** — status` (the form old active.md files used). An unbolded
120
+ * line (`STORY-001 was completed yesterday.`, `STORY-001 — done`) is
121
+ * indistinguishable from a sentence that merely mentions a story, so it is
122
+ * `unknown`, never a confirmed identity.
123
+ *
124
+ * "No active story" as the first visible line is `none` only when the section
125
+ * has no structural `**Story:**` field at all; with one (valid, duplicate or
126
+ * malformed) the section contradicts itself → `unknown` with `contradiction`
127
+ * (PSC-004A, same rule as DOCSYNC's `activeStoryIdentity()`).
128
+ */
96
129
  export declare function parseActiveStoryClaim(excerpt: string | null): StoryClaim;
97
- /** `Active Story: <text>` line of roadmap-index.md. */
130
+ /** `Active Story: <text>` line of roadmap-index.md (outside code fences and comments). */
98
131
  export declare function parseRoadmapActiveStory(content: string): string | null;
99
- /** Table rows `| id | title | STATUS | date |` of roadmap-index.md. */
132
+ /** Table rows `| id | title | STATUS | date |` of roadmap-index.md (outside code fences and comments). */
100
133
  export declare function parseRoadmapStatuses(content: string): Map<string, string>;
101
- /** `### [STORY ]<id> — title` followed by `**Status:** X` in story-index.md. */
134
+ /** `### [STORY ]<id> — title` followed by `**Status:** X` in story-index.md (outside code fences and comments). */
102
135
  export declare function parseStoryIndexStatuses(content: string): Map<string, string>;
136
+ export interface Contradiction {
137
+ message: string;
138
+ documents: string[];
139
+ }
140
+ /**
141
+ * Contradictions between the state documents. Shared by `brief` (freshness /
142
+ * `--reconcile`) and `finalize` (FIN-002 consistency assessment) so that there
143
+ * is exactly one definition of "these documents disagree".
144
+ */
145
+ export declare function findContradictions(ev: Pick<FreshnessEvidence, 'activeStory' | 'roadmapActiveStory' | 'roadmapStatuses' | 'storyIndexStatuses'>): Contradiction[];
103
146
  export declare function evaluateFreshness(ev: FreshnessEvidence): FreshnessResult;
104
147
  //# sourceMappingURL=freshness.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../../src/brief/freshness.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../../src/brief/freshness.ts"],"names":[],"mappings":"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"}
@@ -19,6 +19,8 @@
19
19
  * absence of an active story alone never produce STALE, and the
20
20
  * absence of an active story never means "project complete".
21
21
  */
22
+ import { isIdeaId } from '../ideas/format.js';
23
+ import { declaresNoActiveStory, maskNonContent } from './markdown-context.js';
22
24
  // ---------------------------------------------------------------------------
23
25
  // Work-item ids
24
26
  // ---------------------------------------------------------------------------
@@ -30,8 +32,14 @@
30
32
  const WORK_ITEM_SOURCE = String.raw `(?:[A-Z]{2,}-)+\d{3}[A-Z]?(?:\.\d+)*(?:-[A-Z])?`;
31
33
  /** Decision / lesson records are not work items. */
32
34
  const NON_WORK_PREFIXES = ['ADR-', 'DEC-', 'LESSON-'];
35
+ /**
36
+ * Ideas live in their own namespace (`IDEA-NNN`, src/ideas/format.ts) and are
37
+ * stored in the idea store, not in the state documents. Only the exact idea
38
+ * grammar is excluded: `IDEA-001A`, `IDEA-001B.1` or `IDEA-003-R` are story
39
+ * ids that merely share the prefix and stay work items.
40
+ */
33
41
  function isWorkItem(id) {
34
- return !NON_WORK_PREFIXES.some((p) => id.startsWith(p));
42
+ return !NON_WORK_PREFIXES.some((p) => id.startsWith(p)) && !isIdeaId(id);
35
43
  }
36
44
  export function extractWorkItemIds(text) {
37
45
  const re = new RegExp(`(?<![A-Za-z0-9-])${WORK_ITEM_SOURCE}(?![A-Za-z0-9]|\\.\\d|-[A-Z0-9])`, 'g');
@@ -46,29 +54,69 @@ export function mentionsId(text, id) {
46
54
  const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
47
55
  return new RegExp(`(?<![A-Za-z0-9-])${escaped}(?![A-Za-z0-9]|\\.\\d|-[A-Z0-9])`).test(text);
48
56
  }
57
+ // `maskNonContent` lives in ./markdown-context.ts (shared with story extraction
58
+ // and DOCSYNC) and is re-exported here for existing importers.
59
+ export { maskNonContent };
49
60
  // ---------------------------------------------------------------------------
50
61
  // Parsers for the state documents (confirmed structure only)
51
62
  // ---------------------------------------------------------------------------
52
63
  const NONE_RE = /^\W*(?:no active story|none|unknown)\b|^\s*[—-]\s*$/i;
53
- /** Interprets the `## Current Story` excerpt of active.md. */
64
+ /** Same id grammar as DOCSYNC's `activeStoryIdentity()`. */
65
+ const STORY_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
66
+ /**
67
+ * Interprets the `## Current Story` excerpt of active.md.
68
+ *
69
+ * Identity comes from the structural `**Story:** <id> …` field only (first
70
+ * token = id), exactly as DOCSYNC reads it. Ids in titles, prose, code fences
71
+ * or comments are never identity. Zero usable, several or malformed fields →
72
+ * `unknown` (never a guess, never STALE on its own).
73
+ *
74
+ * Compatibility: a historical section with no `**Story:**` field at all is
75
+ * still read when its first line is a *bold declaration* of exactly one
76
+ * work-item id — `**WIDGET-001 — title**`, `**WIDGET-001**` or
77
+ * `**WIDGET-001** — status` (the form old active.md files used). An unbolded
78
+ * line (`STORY-001 was completed yesterday.`, `STORY-001 — done`) is
79
+ * indistinguishable from a sentence that merely mentions a story, so it is
80
+ * `unknown`, never a confirmed identity.
81
+ *
82
+ * "No active story" as the first visible line is `none` only when the section
83
+ * has no structural `**Story:**` field at all; with one (valid, duplicate or
84
+ * malformed) the section contradicts itself → `unknown` with `contradiction`
85
+ * (PSC-004A, same rule as DOCSYNC's `activeStoryIdentity()`).
86
+ */
54
87
  export function parseActiveStoryClaim(excerpt) {
55
88
  if (excerpt === null)
56
89
  return { kind: 'unknown' };
57
- const firstLine = excerpt.split('\n').find((l) => l.trim() !== '') ?? '';
58
- if (/^\W*no active story\b/i.test(firstLine))
59
- return { kind: 'none' };
60
- const ids = extractWorkItemIds(excerpt);
61
- return ids.length > 0 ? { kind: 'ids', ids } : { kind: 'unknown' };
90
+ const masked = maskNonContent(excerpt);
91
+ const firstLine = masked.split('\n').find((l) => l.trim() !== '') ?? '';
92
+ const fields = masked
93
+ .split('\n')
94
+ .map((l) => /^\*\*Story:\*\*[ \t]*(.*)$/.exec(l.replace(/\r$/, '')))
95
+ .filter((m) => m !== null)
96
+ .map((m) => m[1].trim());
97
+ if (declaresNoActiveStory(excerpt))
98
+ return fields.length === 0 ? { kind: 'none' } : { kind: 'unknown', contradiction: fields };
99
+ if (fields.length > 0) {
100
+ if (fields.length > 1)
101
+ return { kind: 'unknown' };
102
+ const id = /^(\S+)(?:\s|$)/.exec(fields[0])?.[1];
103
+ return id !== undefined && STORY_ID_RE.test(id) ? { kind: 'ids', ids: [id] } : { kind: 'unknown' };
104
+ }
105
+ const line = firstLine.trim();
106
+ const idEnd = String.raw `(?![A-Za-z0-9]|\.\d|-[A-Z0-9])`;
107
+ const declaration = new RegExp(`^\\*\\*(${WORK_ITEM_SOURCE})${idEnd}\\*\\*(?:\\s+[—–-](?:\\s.*)?)?$`).exec(line) ??
108
+ new RegExp(`^\\*\\*(${WORK_ITEM_SOURCE})${idEnd}\\s+[—–-]\\s.*\\*\\*$`).exec(line);
109
+ return declaration !== null && extractWorkItemIds(line).length === 1 ? { kind: 'ids', ids: [declaration[1]] } : { kind: 'unknown' };
62
110
  }
63
- /** `Active Story: <text>` line of roadmap-index.md. */
111
+ /** `Active Story: <text>` line of roadmap-index.md (outside code fences and comments). */
64
112
  export function parseRoadmapActiveStory(content) {
65
- const m = /^Active Story:\s*(.+?)\s*$/m.exec(content);
113
+ const m = /^Active Story:\s*(.+?)\s*$/m.exec(maskNonContent(content));
66
114
  return m ? m[1] : null;
67
115
  }
68
- /** Table rows `| id | title | STATUS | date |` of roadmap-index.md. */
116
+ /** Table rows `| id | title | STATUS | date |` of roadmap-index.md (outside code fences and comments). */
69
117
  export function parseRoadmapStatuses(content) {
70
118
  const out = new Map();
71
- for (const line of content.split('\n')) {
119
+ for (const line of maskNonContent(content).split('\n')) {
72
120
  const cells = line.split('|').map((c) => c.trim());
73
121
  // leading and trailing '|' produce empty first/last cells
74
122
  if (cells.length < 5 || cells[0] !== '' || cells[cells.length - 1] !== '')
@@ -82,10 +130,10 @@ export function parseRoadmapStatuses(content) {
82
130
  }
83
131
  return out;
84
132
  }
85
- /** `### [STORY ]<id> — title` followed by `**Status:** X` in story-index.md. */
133
+ /** `### [STORY ]<id> — title` followed by `**Status:** X` in story-index.md (outside code fences and comments). */
86
134
  export function parseStoryIndexStatuses(content) {
87
135
  const out = new Map();
88
- const lines = content.split('\n');
136
+ const lines = maskNonContent(content).split('\n');
89
137
  for (let i = 0; i < lines.length; i++) {
90
138
  const h = /^###\s+(?:STORY\s+)?([A-Za-z0-9][A-Za-z0-9.\-]*)\s+—/.exec(lines[i]);
91
139
  if (!h)
@@ -115,8 +163,19 @@ function listIds(ids) {
115
163
  function isDone(status) {
116
164
  return status === 'COMPLETED' || status === 'DONE';
117
165
  }
118
- function findContradictions(ev) {
166
+ /**
167
+ * Contradictions between the state documents. Shared by `brief` (freshness /
168
+ * `--reconcile`) and `finalize` (FIN-002 consistency assessment) so that there
169
+ * is exactly one definition of "these documents disagree".
170
+ */
171
+ export function findContradictions(ev) {
119
172
  const out = [];
173
+ if (ev.activeStory.kind === 'unknown' && ev.activeStory.contradiction !== undefined) {
174
+ out.push({
175
+ message: `active.md records no active story, but its Current Story section also has a **Story:** field (${ev.activeStory.contradiction.map((f) => `"${f.slice(0, 40)}"`).join(', ')}).`,
176
+ documents: ['active.md'],
177
+ });
178
+ }
120
179
  if (ev.activeStory.kind === 'none' && ev.roadmapActiveStory !== null && !NONE_RE.test(ev.roadmapActiveStory)) {
121
180
  out.push({
122
181
  message: `active.md records no active story, but roadmap-index.md names "${ev.roadmapActiveStory}".`,
@@ -171,6 +230,7 @@ export function evaluateFreshness(ev) {
171
230
  GIT_NOT_INSTALLED: 'git is not installed or not runnable',
172
231
  NOT_A_GIT_REPOSITORY: 'the project is not inside a git repository',
173
232
  GIT_FAILED: 'git could not be queried',
233
+ GIT_OUTPUT_INVALID: 'git returned history that could not be parsed unambiguously',
174
234
  };
175
235
  reasons.push({ code: 'GIT_UNAVAILABLE', message: `Cannot compare state with history: ${why[ev.git.reason]}.` });
176
236
  gitVerdict = 'unknown';
@@ -187,9 +247,12 @@ export function evaluateFreshness(ev) {
187
247
  base.activityCount = activity.length;
188
248
  const referenced = new Set();
189
249
  let unattributed = 0;
250
+ let knowledgeCommits = 0;
190
251
  for (const c of activity) {
191
252
  const ids = extractWorkItemIds(c.subject);
192
- if (ids.length === 0)
253
+ if (ids.length === 0 && ev.knowledge?.attributed.has(c.sha))
254
+ knowledgeCommits++;
255
+ else if (ids.length === 0)
193
256
  unattributed++;
194
257
  for (const id of ids)
195
258
  referenced.add(id);
@@ -211,6 +274,13 @@ export function evaluateFreshness(ev) {
211
274
  });
212
275
  gitVerdict = 'unknown';
213
276
  }
277
+ else if (ev.git.shallow === true) {
278
+ reasons.push({
279
+ code: 'GIT_HISTORY_SHALLOW',
280
+ message: 'The repository is a shallow clone: history before its boundary is not available, so the check cannot be complete.',
281
+ });
282
+ gitVerdict = 'unknown';
283
+ }
214
284
  else if (unattributed > 0) {
215
285
  reasons.push({
216
286
  code: 'UNATTRIBUTED_ACTIVITY',
@@ -229,11 +299,13 @@ export function evaluateFreshness(ev) {
229
299
  else {
230
300
  reasons.push({
231
301
  code: 'ACTIVITY_RECORDED',
232
- message: `All ${activity.length} commit(s) after the last state update reference work items that the state documents mention.`,
302
+ message: knowledgeCommits === 0
303
+ ? `All ${activity.length} commit(s) after the last state update reference work items that the state documents mention.`
304
+ : `All ${activity.length} commit(s) after the last state update either reference work items that the state documents mention (${activity.length - knowledgeCommits}) or are attributed to registered independent knowledge (${knowledgeCommits}).`,
233
305
  });
234
306
  gitVerdict = 'fresh';
235
307
  }
236
- if (activity.length > 0 && ev.activeStory.kind === 'none') {
308
+ if (activity.length - knowledgeCommits > 0 && ev.activeStory.kind === 'none') {
237
309
  warnings.push('Git activity exists after the last recorded state update, but no active story is recorded.');
238
310
  }
239
311
  }
@@ -1,17 +1,44 @@
1
1
  /**
2
- * Read-only git signals for the freshness check (SELF-AWARENESS-001).
2
+ * Read-only, local, offline git signals for the freshness check
3
+ * (SELF-AWARENESS-001, hardened in alpha.4 by A4-SEC-01/02).
3
4
  *
4
- * Runs only `git log` and `git rev-parse`, never a command that writes, never
5
- * through a shell, with config that could launch helper programs switched off.
6
- * Any failure degrades to `{ status: 'unavailable' }` — git being absent,
7
- * broken or refusing the directory must never fail the brief.
5
+ * Contract:
6
+ * - Only `git log`, `git rev-list`, `git diff-tree`, `git rev-parse` and
7
+ * `git cat-file` are run; never a command that writes, never through a shell.
8
+ * - Offline: lazy fetch of missing objects (partial clones) is disabled
9
+ * (`GIT_NO_LAZY_FETCH=1`) and every transport is refused
10
+ * (`GIT_ALLOW_PROTOCOL=none`), so a missing object is an error here, never a
11
+ * download, an object-store write, or a program started from remote config.
12
+ * (`-c protocol.allow=never` alone was tested and does NOT stop a `file`
13
+ * promisor remote; it is kept only as an additional layer.)
14
+ * - Structural parsing: output that carries attacker-influenced text (commit
15
+ * subjects, file names) is never split on characters that text can contain.
16
+ * Records use NUL (`-z`), which neither a subject nor a path can contain;
17
+ * commit metadata and file lists come from separate commands; shas and dates
18
+ * are validated against strict patterns; the commit list is cross-checked
19
+ * against `git rev-list`. Anything unexpected is `GIT_OUTPUT_INVALID`.
20
+ * - Fail closed: every failure degrades to `{ status: 'unavailable' }`, which
21
+ * freshness turns into UNKNOWN. Git being absent, broken, refusing the
22
+ * directory or returning something unparseable never fails the brief and is
23
+ * never evidence of FRESH.
8
24
  */
9
25
  import type { GitSignals } from './freshness.js';
10
26
  /** Upper bound on commits read after the last state update. Hitting it marks the result truncated. */
11
27
  export declare const MAX_COMMITS_SINCE = 500;
28
+ /** Largest blob `readBlobAt` will read; checked with `git cat-file -s` before the content is requested. */
29
+ export declare const MAX_BLOB_BYTES: number;
30
+ export declare function isGitSha(s: string): boolean;
12
31
  /**
13
32
  * @param targetDir project directory (git is run from here; paths are relative to it)
14
33
  * @param stateFiles state documents, relative to `targetDir`. Only existing ones need be passed.
15
34
  */
16
35
  export declare function readGitSignals(targetDir: string, stateFiles: string[]): GitSignals;
36
+ /**
37
+ * Contents of `relPath` (relative to `targetDir`) as of commit `sha`, or null
38
+ * when the file does not exist there, is larger than `MAX_BLOB_BYTES`, is not
39
+ * available locally (a partial clone's missing blob is NOT fetched) or cannot be
40
+ * read. `git cat-file` only: read-only, no textconv, no shell. Used by KA-001 to
41
+ * judge an idea registration as it was when the commit was made.
42
+ */
43
+ export declare function readBlobAt(targetDir: string, sha: string, relPath: string): string | null;
17
44
  //# sourceMappingURL=git-signals.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"git-signals.d.ts","sourceRoot":"","sources":["../../src/brief/git-signals.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAmB,MAAM,gBAAgB,CAAC;AAElE,sGAAsG;AACtG,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAgCrC;;;GAGG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,UAAU,CAqDlF"}
1
+ {"version":3,"file":"git-signals.d.ts","sourceRoot":"","sources":["../../src/brief/git-signals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAmB,MAAM,gBAAgB,CAAC;AAElE,sGAAsG;AACtG,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,2GAA2G;AAC3G,eAAO,MAAM,cAAc,QAAa,CAAC;AAqDzC,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAE3C;AAyDD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,UAAU,CA6DlF;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUzF"}