@latysh-development/monolith 0.1.0-alpha.3 → 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.
- package/README.md +1 -1
- package/dist/brief/cli.d.ts.map +1 -1
- package/dist/brief/cli.js +8 -2
- package/dist/brief/collect.d.ts.map +1 -1
- package/dist/brief/collect.js +11 -3
- package/dist/brief/freshness.d.ts +17 -2
- package/dist/brief/freshness.d.ts.map +1 -1
- package/dist/brief/freshness.js +22 -4
- package/dist/brief/git-signals.d.ts +32 -5
- package/dist/brief/git-signals.d.ts.map +1 -1
- package/dist/brief/git-signals.js +169 -39
- package/dist/brief/knowledge.d.ts +62 -0
- package/dist/brief/knowledge.d.ts.map +1 -0
- package/dist/brief/knowledge.js +123 -0
- package/dist/brief/reconcile.d.ts +7 -1
- package/dist/brief/reconcile.d.ts.map +1 -1
- package/dist/brief/reconcile.js +42 -2
- package/dist/cli/index.js +1 -1
- package/dist/cli/style.d.ts +26 -0
- package/dist/cli/style.d.ts.map +1 -0
- package/dist/cli/style.js +76 -0
- package/dist/cli/ui.d.ts.map +1 -1
- package/dist/cli/ui.js +33 -6
- package/dist/coordinatorium/cli.d.ts.map +1 -1
- package/dist/coordinatorium/cli.js +13 -0
- package/dist/finalize/cli.d.ts.map +1 -1
- package/dist/finalize/cli.js +30 -13
- package/dist/finalize/consistency.d.ts +51 -0
- package/dist/finalize/consistency.d.ts.map +1 -0
- package/dist/finalize/consistency.js +57 -0
- package/dist/finalize/core.d.ts +26 -0
- package/dist/finalize/core.d.ts.map +1 -1
- package/dist/finalize/core.js +140 -1
- package/dist/ideas/cli.d.ts +1 -0
- package/dist/ideas/cli.d.ts.map +1 -1
- package/dist/ideas/cli.js +14 -3
- package/dist/ideas/format.d.ts +31 -1
- package/dist/ideas/format.d.ts.map +1 -1
- package/dist/ideas/format.js +109 -1
- package/dist/ideas/store.d.ts +2 -0
- package/dist/ideas/store.d.ts.map +1 -1
- package/dist/ideas/store.js +4 -1
- package/dist/ideas/types.d.ts +10 -0
- package/dist/ideas/types.d.ts.map +1 -1
- package/dist/ideas/types.js +3 -0
- package/dist/readmodel/dashboard.d.ts.map +1 -1
- package/dist/readmodel/dashboard.js +19 -1
- package/dist/readmodel/index.d.ts +2 -1
- package/dist/readmodel/index.d.ts.map +1 -1
- package/dist/readmodel/index.js +1 -0
- package/dist/readmodel/roadmap.d.ts +34 -0
- package/dist/readmodel/roadmap.d.ts.map +1 -0
- package/dist/readmodel/roadmap.js +254 -0
- package/dist/readmodel/types.d.ts +88 -0
- package/dist/readmodel/types.d.ts.map +1 -1
- package/dist/root/safe-write.d.ts +15 -4
- package/dist/root/safe-write.d.ts.map +1 -1
- package/dist/root/safe-write.js +264 -50
- package/dist/ui/assets/index-BJKxUDPD.css +1 -0
- package/dist/ui/assets/index-ioC9DPHQ.js +28 -0
- package/dist/ui/index.html +2 -2
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js +4 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -2
- package/dist/ui/assets/index-5zXAgig8.css +0 -1
- package/dist/ui/assets/index-CQgoMYLM.js +0 -28
package/README.md
CHANGED
|
@@ -183,7 +183,7 @@ listing — not an official partnership, endorsement or certification.
|
|
|
183
183
|
|
|
184
184
|
## Project Status
|
|
185
185
|
|
|
186
|
-
**Alpha** (`0.1.0-alpha.
|
|
186
|
+
**Alpha** (`0.1.0-alpha.4`). The API and CLI are experimental and may change
|
|
187
187
|
without notice. Published to npm under the alpha dist-tag. Durable State and the Coordinatorium
|
|
188
188
|
(`monolith durable`, `monolith mission`) are experimental and are not part of
|
|
189
189
|
the Quick Start.
|
package/dist/brief/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/brief/cli.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;GAcG;
|
|
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;AAyBH,OAAO,KAAK,EACV,UAAU,EAOX,MAAM,YAAY,CAAC;
|
|
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"}
|
package/dist/brief/collect.js
CHANGED
|
@@ -20,8 +20,9 @@ import { maskNonContent } from './markdown-context.js';
|
|
|
20
20
|
import { selectActiveMissions, selectRecentFinishedMissions, selectLatestCheckpoint, hasCheckpointOrderMismatch, } from './selectors.js';
|
|
21
21
|
import { stateFilePath, validateState, createInitialState, MAX_STATE_BYTES } from '../coordinatorium/index.js';
|
|
22
22
|
import { ACTIVE_MD_REL, PARKED_IDEAS_LIMIT, PROTOCOLS_DIR, ROADMAP_INDEX_REL, STORY_INDEX_REL, HANDOFF_REL } from './constants.js';
|
|
23
|
-
import { evaluateFreshness, parseActiveStoryClaim, parseRoadmapActiveStory, parseRoadmapStatuses, parseStoryIndexStatuses, } from './freshness.js';
|
|
24
|
-
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';
|
|
25
26
|
import { ideaStore, listIdeas } from '../ideas/index.js';
|
|
26
27
|
import { ideaNumber } from '../ideas/format.js';
|
|
27
28
|
const RECENT_FINISHED_MISSIONS_LIMIT = 5;
|
|
@@ -270,8 +271,15 @@ function collectFreshness(targetDir, monolithRoot, root, story) {
|
|
|
270
271
|
const content = new Map(docs.map((rel) => [rel, readStateDocument(targetDir, monolithRoot, root, rel)]));
|
|
271
272
|
const roadmap = content.get(ROADMAP_INDEX_REL) ?? null;
|
|
272
273
|
const storyIndex = content.get(STORY_INDEX_REL) ?? null;
|
|
274
|
+
const git = readGitSignals(targetDir, stateFiles);
|
|
273
275
|
const evidence = {
|
|
274
|
-
git
|
|
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
|
+
}),
|
|
275
283
|
activeStory: story.fileExists && story.hasCurrentStorySection ? parseActiveStoryClaim(story.excerpt) : { kind: 'unknown' },
|
|
276
284
|
roadmapActiveStory: roadmap === null ? null : parseRoadmapActiveStory(roadmap),
|
|
277
285
|
roadmapStatuses: roadmap === null ? new Map() : parseRoadmapStatuses(roadmap),
|
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
* absence of an active story never means "project complete".
|
|
21
21
|
*/
|
|
22
22
|
import { maskNonContent } from './markdown-context.js';
|
|
23
|
+
import type { KnowledgeEvidence } from './knowledge.js';
|
|
23
24
|
export type FreshnessStatus = 'FRESH' | 'STALE' | 'UNKNOWN';
|
|
24
|
-
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';
|
|
25
26
|
export interface FreshnessReason {
|
|
26
27
|
code: FreshnessReasonCode;
|
|
27
28
|
message: string;
|
|
@@ -37,7 +38,7 @@ export interface GitCommitSignal {
|
|
|
37
38
|
/** Paths changed by the commit, relative to the project directory. Empty for plain merges. */
|
|
38
39
|
files: string[];
|
|
39
40
|
}
|
|
40
|
-
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';
|
|
41
42
|
export type GitSignals = {
|
|
42
43
|
status: 'unavailable';
|
|
43
44
|
reason: GitUnavailableReason;
|
|
@@ -52,6 +53,8 @@ export type GitSignals = {
|
|
|
52
53
|
commitsSince: GitCommitSignal[];
|
|
53
54
|
/** True when git history was cut at the read limit — the picture is partial. */
|
|
54
55
|
truncated: boolean;
|
|
56
|
+
/** True in a shallow clone: history before the boundary is absent, so "no later activity" proves nothing. */
|
|
57
|
+
shallow?: boolean;
|
|
55
58
|
};
|
|
56
59
|
export type StoryClaim = {
|
|
57
60
|
kind: 'none';
|
|
@@ -80,6 +83,8 @@ export interface FreshnessEvidence {
|
|
|
80
83
|
recordedText: string;
|
|
81
84
|
/** Repo-relative-to-project paths that are state documents (their own commits are not "activity"). */
|
|
82
85
|
stateFiles: ReadonlySet<string>;
|
|
86
|
+
/** KA-001: commits explained by registered independent knowledge. Absent = none (older callers). */
|
|
87
|
+
knowledge?: KnowledgeEvidence;
|
|
83
88
|
}
|
|
84
89
|
export interface FreshnessResult {
|
|
85
90
|
status: FreshnessStatus;
|
|
@@ -128,5 +133,15 @@ export declare function parseRoadmapActiveStory(content: string): string | null;
|
|
|
128
133
|
export declare function parseRoadmapStatuses(content: string): Map<string, string>;
|
|
129
134
|
/** `### [STORY ]<id> — title` followed by `**Status:** X` in story-index.md (outside code fences and comments). */
|
|
130
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[];
|
|
131
146
|
export declare function evaluateFreshness(ev: FreshnessEvidence): FreshnessResult;
|
|
132
147
|
//# sourceMappingURL=freshness.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../../src/brief/freshness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAyB,cAAc,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../../src/brief/freshness.ts"],"names":[],"mappings":"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"}
|
package/dist/brief/freshness.js
CHANGED
|
@@ -163,7 +163,12 @@ function listIds(ids) {
|
|
|
163
163
|
function isDone(status) {
|
|
164
164
|
return status === 'COMPLETED' || status === 'DONE';
|
|
165
165
|
}
|
|
166
|
-
|
|
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) {
|
|
167
172
|
const out = [];
|
|
168
173
|
if (ev.activeStory.kind === 'unknown' && ev.activeStory.contradiction !== undefined) {
|
|
169
174
|
out.push({
|
|
@@ -225,6 +230,7 @@ export function evaluateFreshness(ev) {
|
|
|
225
230
|
GIT_NOT_INSTALLED: 'git is not installed or not runnable',
|
|
226
231
|
NOT_A_GIT_REPOSITORY: 'the project is not inside a git repository',
|
|
227
232
|
GIT_FAILED: 'git could not be queried',
|
|
233
|
+
GIT_OUTPUT_INVALID: 'git returned history that could not be parsed unambiguously',
|
|
228
234
|
};
|
|
229
235
|
reasons.push({ code: 'GIT_UNAVAILABLE', message: `Cannot compare state with history: ${why[ev.git.reason]}.` });
|
|
230
236
|
gitVerdict = 'unknown';
|
|
@@ -241,9 +247,12 @@ export function evaluateFreshness(ev) {
|
|
|
241
247
|
base.activityCount = activity.length;
|
|
242
248
|
const referenced = new Set();
|
|
243
249
|
let unattributed = 0;
|
|
250
|
+
let knowledgeCommits = 0;
|
|
244
251
|
for (const c of activity) {
|
|
245
252
|
const ids = extractWorkItemIds(c.subject);
|
|
246
|
-
if (ids.length === 0)
|
|
253
|
+
if (ids.length === 0 && ev.knowledge?.attributed.has(c.sha))
|
|
254
|
+
knowledgeCommits++;
|
|
255
|
+
else if (ids.length === 0)
|
|
247
256
|
unattributed++;
|
|
248
257
|
for (const id of ids)
|
|
249
258
|
referenced.add(id);
|
|
@@ -265,6 +274,13 @@ export function evaluateFreshness(ev) {
|
|
|
265
274
|
});
|
|
266
275
|
gitVerdict = 'unknown';
|
|
267
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
|
+
}
|
|
268
284
|
else if (unattributed > 0) {
|
|
269
285
|
reasons.push({
|
|
270
286
|
code: 'UNATTRIBUTED_ACTIVITY',
|
|
@@ -283,11 +299,13 @@ export function evaluateFreshness(ev) {
|
|
|
283
299
|
else {
|
|
284
300
|
reasons.push({
|
|
285
301
|
code: 'ACTIVITY_RECORDED',
|
|
286
|
-
message:
|
|
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}).`,
|
|
287
305
|
});
|
|
288
306
|
gitVerdict = 'fresh';
|
|
289
307
|
}
|
|
290
|
-
if (activity.length > 0 && ev.activeStory.kind === 'none') {
|
|
308
|
+
if (activity.length - knowledgeCommits > 0 && ev.activeStory.kind === 'none') {
|
|
291
309
|
warnings.push('Git activity exists after the last recorded state update, but no active story is recorded.');
|
|
292
310
|
}
|
|
293
311
|
}
|
|
@@ -1,17 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Read-only git signals for the freshness check
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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"}
|
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Read-only git signals for the freshness check
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 { execFileSync } from 'child_process';
|
|
10
26
|
/** Upper bound on commits read after the last state update. Hitting it marks the result truncated. */
|
|
11
27
|
export const MAX_COMMITS_SINCE = 500;
|
|
28
|
+
/** Largest blob `readBlobAt` will read; checked with `git cat-file -s` before the content is requested. */
|
|
29
|
+
export const MAX_BLOB_BYTES = 256 * 1024;
|
|
12
30
|
const GIT_TIMEOUT_MS = 10_000;
|
|
13
31
|
const GIT_MAX_BUFFER = 8 * 1024 * 1024;
|
|
14
32
|
const SAFE_CONFIG = [
|
|
@@ -16,18 +34,37 @@ const SAFE_CONFIG = [
|
|
|
16
34
|
'-c', 'core.pager=cat',
|
|
17
35
|
'-c', 'log.showSignature=false',
|
|
18
36
|
'-c', 'diff.external=',
|
|
37
|
+
'-c', 'core.quotepath=off',
|
|
38
|
+
'-c', 'protocol.allow=never',
|
|
39
|
+
'-c', 'protocol.file.allow=never',
|
|
19
40
|
];
|
|
41
|
+
const SAFE_ENV = {
|
|
42
|
+
GIT_OPTIONAL_LOCKS: '0',
|
|
43
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
44
|
+
GIT_EXTERNAL_DIFF: '',
|
|
45
|
+
/** git >= 2.44: a missing object is an error instead of a fetch from the promisor remote. */
|
|
46
|
+
GIT_NO_LAZY_FETCH: '1',
|
|
47
|
+
/** Older git has no GIT_NO_LAZY_FETCH; refusing every transport stops the fetch itself. */
|
|
48
|
+
GIT_ALLOW_PROTOCOL: 'none',
|
|
49
|
+
/** State-document names come from config: never git pathspec magic. */
|
|
50
|
+
GIT_LITERAL_PATHSPECS: '1',
|
|
51
|
+
/** Replace refs would change the history this check reports. */
|
|
52
|
+
GIT_NO_REPLACE_OBJECTS: '1',
|
|
53
|
+
};
|
|
20
54
|
class GitMissingError extends Error {
|
|
21
55
|
}
|
|
22
|
-
|
|
56
|
+
class GitOutputError extends Error {
|
|
57
|
+
}
|
|
58
|
+
function git(cwd, args, opts = {}) {
|
|
23
59
|
try {
|
|
24
60
|
return execFileSync('git', [...SAFE_CONFIG, ...args], {
|
|
25
61
|
cwd,
|
|
26
62
|
encoding: 'utf8',
|
|
27
63
|
timeout: GIT_TIMEOUT_MS,
|
|
28
|
-
maxBuffer: GIT_MAX_BUFFER,
|
|
29
|
-
stdio: ['
|
|
30
|
-
|
|
64
|
+
maxBuffer: opts.maxBuffer ?? GIT_MAX_BUFFER,
|
|
65
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
66
|
+
input: opts.input ?? '',
|
|
67
|
+
env: { ...process.env, ...SAFE_ENV },
|
|
31
68
|
});
|
|
32
69
|
}
|
|
33
70
|
catch (err) {
|
|
@@ -36,8 +73,76 @@ function git(cwd, args) {
|
|
|
36
73
|
throw err;
|
|
37
74
|
}
|
|
38
75
|
}
|
|
39
|
-
const
|
|
40
|
-
const
|
|
76
|
+
const SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
77
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+-]\d{2}:\d{2})$/;
|
|
78
|
+
/** `:oldmode newmode sha sha STATUS` (and the combined-diff form `::m m m sha sha sha SS`). Never a file name: names are consumed as the token after one. */
|
|
79
|
+
const RAW_HEADER_RE = /^:+[0-7]{6}(?: [0-7]{6})* [0-9a-f]{4,64}(?: [0-9a-f]{4,64})* [A-Z]+[0-9]*$/;
|
|
80
|
+
export function isGitSha(s) {
|
|
81
|
+
return SHA_RE.test(s);
|
|
82
|
+
}
|
|
83
|
+
/** Tokens of NUL-terminated output. The output must be empty or end with NUL. */
|
|
84
|
+
function nulTokens(out) {
|
|
85
|
+
if (out === '')
|
|
86
|
+
return [];
|
|
87
|
+
if (!out.endsWith('\0'))
|
|
88
|
+
throw new GitOutputError('output is not NUL-terminated');
|
|
89
|
+
return out.slice(0, -1).split('\0');
|
|
90
|
+
}
|
|
91
|
+
/** `git log -z --no-patch --format=%H%x00%cI%x00%s`: strict triples. */
|
|
92
|
+
function parseCommitTriples(out) {
|
|
93
|
+
const t = nulTokens(out);
|
|
94
|
+
if (t.length % 3 !== 0)
|
|
95
|
+
throw new GitOutputError('commit records are not complete triples');
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
const commits = [];
|
|
98
|
+
for (let i = 0; i < t.length; i += 3) {
|
|
99
|
+
const [sha, date, subject] = [t[i], t[i + 1], t[i + 2]];
|
|
100
|
+
if (!SHA_RE.test(sha))
|
|
101
|
+
throw new GitOutputError('a commit id is not a valid object name');
|
|
102
|
+
if (!DATE_RE.test(date))
|
|
103
|
+
throw new GitOutputError('a commit date is not an ISO-8601 date');
|
|
104
|
+
if (seen.has(sha))
|
|
105
|
+
throw new GitOutputError('a commit appears twice');
|
|
106
|
+
seen.add(sha);
|
|
107
|
+
commits.push({ sha, date, subject });
|
|
108
|
+
}
|
|
109
|
+
return commits;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* `git diff-tree --stdin -z --raw`: `<sha> NUL` then zero or more
|
|
113
|
+
* `<:raw-header> NUL <path> NUL`. A path is only ever read as the token after a
|
|
114
|
+
* raw header, so no file name (spaces, newlines, control characters, even a
|
|
115
|
+
* 40-hex string) can be mistaken for a commit id or a header. Commits without
|
|
116
|
+
* changes (clean merges, empty commits) may be absent; order must be preserved.
|
|
117
|
+
*/
|
|
118
|
+
function parseFileLists(out, expected, prefix) {
|
|
119
|
+
const t = nulTokens(out);
|
|
120
|
+
const files = new Map();
|
|
121
|
+
let next = 0; // index into `expected` the next sha must be at or after
|
|
122
|
+
let i = 0;
|
|
123
|
+
while (i < t.length) {
|
|
124
|
+
const sha = t[i++];
|
|
125
|
+
const at = SHA_RE.test(sha) ? expected.indexOf(sha, next) : -1;
|
|
126
|
+
if (at === -1)
|
|
127
|
+
throw new GitOutputError('a file list belongs to an unexpected or out-of-order commit');
|
|
128
|
+
next = at + 1;
|
|
129
|
+
const list = [];
|
|
130
|
+
while (i < t.length && t[i].startsWith(':')) {
|
|
131
|
+
if (!RAW_HEADER_RE.test(t[i]) || i + 1 >= t.length)
|
|
132
|
+
throw new GitOutputError('a raw diff entry is malformed');
|
|
133
|
+
// `--relative` is ignored for combined (merge) entries, which start with `::`: make them project-relative here.
|
|
134
|
+
if (t[i].startsWith('::')) {
|
|
135
|
+
if (t[i + 1].startsWith(prefix))
|
|
136
|
+
list.push(t[i + 1].slice(prefix.length));
|
|
137
|
+
}
|
|
138
|
+
else
|
|
139
|
+
list.push(t[i + 1]);
|
|
140
|
+
i += 2;
|
|
141
|
+
}
|
|
142
|
+
files.set(sha, list);
|
|
143
|
+
}
|
|
144
|
+
return files;
|
|
145
|
+
}
|
|
41
146
|
/**
|
|
42
147
|
* @param targetDir project directory (git is run from here; paths are relative to it)
|
|
43
148
|
* @param stateFiles state documents, relative to `targetDir`. Only existing ones need be passed.
|
|
@@ -56,46 +161,71 @@ export function readGitSignals(targetDir, stateFiles) {
|
|
|
56
161
|
}
|
|
57
162
|
try {
|
|
58
163
|
// A repository without commits has no HEAD: nothing to compare with.
|
|
164
|
+
let head;
|
|
59
165
|
try {
|
|
60
|
-
git(targetDir, ['rev-parse', '--verify', '--quiet', 'HEAD']);
|
|
166
|
+
head = git(targetDir, ['rev-parse', '--verify', '--quiet', 'HEAD']).trim();
|
|
61
167
|
}
|
|
62
168
|
catch {
|
|
63
169
|
return { status: 'ok', lastStateUpdate: null, commitsSince: [], truncated: false };
|
|
64
170
|
}
|
|
171
|
+
if (!SHA_RE.test(head))
|
|
172
|
+
throw new GitOutputError('HEAD did not resolve to an object name');
|
|
173
|
+
// A shallow clone has no history before its boundary: "no later commit" proves nothing.
|
|
174
|
+
const shallowOut = git(targetDir, ['rev-parse', '--is-shallow-repository']).trim();
|
|
175
|
+
if (shallowOut !== 'true' && shallowOut !== 'false')
|
|
176
|
+
throw new GitOutputError('unexpected shallow-repository answer');
|
|
177
|
+
const shallow = shallowOut === 'true';
|
|
65
178
|
let lastStateUpdate = null;
|
|
66
179
|
if (stateFiles.length > 0) {
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
180
|
+
const t = nulTokens(git(targetDir, ['log', '-1', '-z', '--no-patch', '--format=%H%x00%cI', head, '--', ...stateFiles]));
|
|
181
|
+
if (t.length !== 0) {
|
|
182
|
+
if (t.length !== 2 || !SHA_RE.test(t[0]) || !DATE_RE.test(t[1]))
|
|
183
|
+
throw new GitOutputError('the last state update is not a valid record');
|
|
184
|
+
lastStateUpdate = { sha: t[0], date: t[1] };
|
|
71
185
|
}
|
|
72
186
|
}
|
|
73
187
|
if (!lastStateUpdate)
|
|
74
|
-
return { status: 'ok', lastStateUpdate: null, commitsSince: [], truncated: false };
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
'
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
continue;
|
|
89
|
-
const nl = rec.indexOf('\n');
|
|
90
|
-
const header = nl === -1 ? rec : rec.slice(0, nl);
|
|
91
|
-
const [sha, date, subject] = header.split(US);
|
|
92
|
-
const files = (nl === -1 ? '' : rec.slice(nl + 1)).split('\n').filter((l) => l.trim() !== '');
|
|
93
|
-
commits.push({ sha, date, subject: subject ?? '', files });
|
|
188
|
+
return { status: 'ok', lastStateUpdate: null, commitsSince: [], truncated: false, shallow };
|
|
189
|
+
const range = `${lastStateUpdate.sha}..${head}`;
|
|
190
|
+
const limit = `-n${MAX_COMMITS_SINCE + 1}`;
|
|
191
|
+
const commits = parseCommitTriples(git(targetDir, ['log', '-z', '--no-patch', limit, '--format=%H%x00%cI%x00%s', range]));
|
|
192
|
+
// Independent cross-check of the commit list: same range, no free text in the output.
|
|
193
|
+
const listed = nulTokens(git(targetDir, ['rev-list', '-z', limit, range]));
|
|
194
|
+
if (listed.length !== commits.length || listed.some((s, k) => s !== commits[k].sha)) {
|
|
195
|
+
throw new GitOutputError('the commit list disagrees with git rev-list');
|
|
196
|
+
}
|
|
197
|
+
let fileLists = new Map();
|
|
198
|
+
if (commits.length > 0) {
|
|
199
|
+
const prefix = git(targetDir, ['rev-parse', '--show-prefix']).replace(/\n$/, '');
|
|
200
|
+
const raw = git(targetDir, ['diff-tree', '--stdin', '-z', '-r', '--raw', '-c', '--root', '--no-renames', '--no-ext-diff', '--no-textconv', '--ignore-submodules=none', '--relative'], { input: `${commits.map((c) => c.sha).join('\n')}\n` });
|
|
201
|
+
fileLists = parseFileLists(raw, commits.map((c) => c.sha), prefix);
|
|
94
202
|
}
|
|
95
|
-
const
|
|
96
|
-
|
|
203
|
+
const signals = commits.map((c) => ({ sha: c.sha, date: c.date, subject: c.subject, files: fileLists.get(c.sha) ?? [] }));
|
|
204
|
+
const truncated = signals.length > MAX_COMMITS_SINCE;
|
|
205
|
+
return { status: 'ok', lastStateUpdate, commitsSince: signals.slice(0, MAX_COMMITS_SINCE), truncated, shallow };
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
return { status: 'unavailable', reason: err instanceof GitOutputError ? 'GIT_OUTPUT_INVALID' : 'GIT_FAILED' };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Contents of `relPath` (relative to `targetDir`) as of commit `sha`, or null
|
|
213
|
+
* when the file does not exist there, is larger than `MAX_BLOB_BYTES`, is not
|
|
214
|
+
* available locally (a partial clone's missing blob is NOT fetched) or cannot be
|
|
215
|
+
* read. `git cat-file` only: read-only, no textconv, no shell. Used by KA-001 to
|
|
216
|
+
* judge an idea registration as it was when the commit was made.
|
|
217
|
+
*/
|
|
218
|
+
export function readBlobAt(targetDir, sha, relPath) {
|
|
219
|
+
if (!SHA_RE.test(sha) || relPath.includes('\0') || relPath.startsWith('-'))
|
|
220
|
+
return null;
|
|
221
|
+
const spec = `${sha}:./${relPath}`;
|
|
222
|
+
try {
|
|
223
|
+
const size = git(targetDir, ['cat-file', '-s', spec]).trim();
|
|
224
|
+
if (!/^\d{1,9}$/.test(size) || Number(size) > MAX_BLOB_BYTES)
|
|
225
|
+
return null;
|
|
226
|
+
return git(targetDir, ['cat-file', 'blob', spec], { maxBuffer: MAX_BLOB_BYTES + 1024 });
|
|
97
227
|
}
|
|
98
228
|
catch {
|
|
99
|
-
return
|
|
229
|
+
return null;
|
|
100
230
|
}
|
|
101
231
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Independent knowledge attribution (KA-001).
|
|
3
|
+
*
|
|
4
|
+
* A project may keep knowledge that belongs to no work item — a strategy
|
|
5
|
+
* paper, a vision note — without that being "unattributed activity" and
|
|
6
|
+
* without pretending it is part of the active story. The registration is an
|
|
7
|
+
* idea of the Idea Store (`IDEA-NNN`) that declares the artifact's `paths`.
|
|
8
|
+
*
|
|
9
|
+
* A commit is attributed to independent knowledge only when ALL hold:
|
|
10
|
+
* 1. its subject names no work item (a work item keeps its own semantics);
|
|
11
|
+
* 2. it names at least one `IDEA-NNN` that is a valid idea file **in the
|
|
12
|
+
* tree of that very commit** (so registering an idea later never
|
|
13
|
+
* legalises older commits, and a commit that registers or edits the idea
|
|
14
|
+
* is itself never attributed — its own files are not covered);
|
|
15
|
+
* 3. the ideas it names declare `paths`, every declared path passes the
|
|
16
|
+
* attribution policy (exact document files only — no directory prefixes, no
|
|
17
|
+
* source code, nothing inside the MONOLITH root; `attributionPathProblem`),
|
|
18
|
+
* and EVERY file the commit changed (other than state documents) is covered
|
|
19
|
+
* by those paths;
|
|
20
|
+
* 4. no changed file lies inside the MONOLITH root;
|
|
21
|
+
* 5. it names at most MAX_IDEA_IDS_PER_COMMIT ideas, and the run stays inside a
|
|
22
|
+
* fixed budget of blob reads (each read is a git subprocess).
|
|
23
|
+
* Attribution only EXPLAINS a commit. It is not approval: a registered, correctly
|
|
24
|
+
* attributed change may still be wrong or unreviewed.
|
|
25
|
+
* Anything else stays exactly what it was: unattributed activity. Nothing here
|
|
26
|
+
* activates a story, touches the roadmap or marks anything done; it only
|
|
27
|
+
* decides whether a commit is *explained*. No LLM, no network, read-only git.
|
|
28
|
+
*/
|
|
29
|
+
import type { GitSignals } from './freshness.js';
|
|
30
|
+
export interface KnowledgeAttribution {
|
|
31
|
+
/** Idea ids named in the subject that were valid in the commit's tree. */
|
|
32
|
+
ideaIds: string[];
|
|
33
|
+
/** The paths those ideas declared (what the attribution rests on), in declaration order, without repeats. */
|
|
34
|
+
paths: string[];
|
|
35
|
+
}
|
|
36
|
+
export interface KnowledgeRejection {
|
|
37
|
+
/** One short reason a person can check. */
|
|
38
|
+
reason: string;
|
|
39
|
+
}
|
|
40
|
+
export interface KnowledgeEvidence {
|
|
41
|
+
/** Commit sha → attribution. Only commits that satisfied every rule. */
|
|
42
|
+
attributed: ReadonlyMap<string, KnowledgeAttribution>;
|
|
43
|
+
/** Commit sha → why a commit that NAMED an idea was not attributed. */
|
|
44
|
+
rejected: ReadonlyMap<string, KnowledgeRejection>;
|
|
45
|
+
}
|
|
46
|
+
export declare const EMPTY_KNOWLEDGE: KnowledgeEvidence;
|
|
47
|
+
/** Upper bound of commits examined; beyond it the rest simply stay unattributed. */
|
|
48
|
+
export declare const MAX_KNOWLEDGE_COMMITS = 200;
|
|
49
|
+
/** A subject naming more ideas than this is refused without any git call (A4-SEC-05). */
|
|
50
|
+
export declare const MAX_IDEA_IDS_PER_COMMIT = 8;
|
|
51
|
+
/** Total blob reads (git subprocesses) per run; beyond it commits stay unattributed. */
|
|
52
|
+
export declare const MAX_KNOWLEDGE_BLOB_READS = 64;
|
|
53
|
+
export declare function ideaIdsInSubject(subject: string): string[];
|
|
54
|
+
/** Reads `relPath` as of commit `sha`; null when it does not exist there (or cannot be read). */
|
|
55
|
+
export type BlobReader = (sha: string, relPath: string) => string | null;
|
|
56
|
+
export declare function attributeKnowledge(git: GitSignals, opts: {
|
|
57
|
+
monolithRoot: string;
|
|
58
|
+
stateFiles: ReadonlySet<string>;
|
|
59
|
+
readBlob: BlobReader;
|
|
60
|
+
hasWorkItem: (subject: string) => boolean;
|
|
61
|
+
}): KnowledgeEvidence;
|
|
62
|
+
//# sourceMappingURL=knowledge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knowledge.d.ts","sourceRoot":"","sources":["../../src/brief/knowledge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,KAAK,EAAmB,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAElE,MAAM,WAAW,oBAAoB;IACnC,0EAA0E;IAC1E,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,6GAA6G;IAC7G,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;IACtD,uEAAuE;IACvE,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CACnD;AAED,eAAO,MAAM,eAAe,EAAE,iBAAkE,CAAC;AAEjG,oFAAoF;AACpF,eAAO,MAAM,qBAAqB,MAAM,CAAC;AACzC,yFAAyF;AACzF,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC,wFAAwF;AACxF,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAI3C,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAE1D;AAED,iGAAiG;AACjG,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAEzE,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,UAAU,EACf,IAAI,EAAE;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,UAAU,CAAC;IAAC,WAAW,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAA;CAAE,GAC/H,iBAAiB,CAyBnB"}
|