@latysh-development/monolith 0.0.0-stage → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +179 -2
- package/dist/brief/cli.d.ts +30 -0
- package/dist/brief/cli.d.ts.map +1 -0
- package/dist/brief/cli.js +85 -0
- package/dist/brief/collect.d.ts +17 -0
- package/dist/brief/collect.d.ts.map +1 -0
- package/dist/brief/collect.js +342 -0
- package/dist/brief/config.d.ts +40 -0
- package/dist/brief/config.d.ts.map +1 -0
- package/dist/brief/config.js +85 -0
- package/dist/brief/constants.d.ts +12 -0
- package/dist/brief/constants.d.ts.map +1 -0
- package/dist/brief/constants.js +11 -0
- package/dist/brief/docs.d.ts +53 -0
- package/dist/brief/docs.d.ts.map +1 -0
- package/dist/brief/docs.js +229 -0
- package/dist/brief/freshness.d.ts +104 -0
- package/dist/brief/freshness.d.ts.map +1 -0
- package/dist/brief/freshness.js +263 -0
- package/dist/brief/git-signals.d.ts +17 -0
- package/dist/brief/git-signals.d.ts.map +1 -0
- package/dist/brief/git-signals.js +101 -0
- package/dist/brief/index.d.ts +11 -0
- package/dist/brief/index.d.ts.map +1 -0
- package/dist/brief/index.js +5 -0
- package/dist/brief/reconcile.d.ts +75 -0
- package/dist/brief/reconcile.d.ts.map +1 -0
- package/dist/brief/reconcile.js +259 -0
- package/dist/brief/render.d.ts +67 -0
- package/dist/brief/render.d.ts.map +1 -0
- package/dist/brief/render.js +458 -0
- package/dist/brief/sanitize.d.ts +38 -0
- package/dist/brief/sanitize.d.ts.map +1 -0
- package/dist/brief/sanitize.js +48 -0
- package/dist/brief/selectors.d.ts +36 -0
- package/dist/brief/selectors.d.ts.map +1 -0
- package/dist/brief/selectors.js +75 -0
- package/dist/brief/story.d.ts +18 -0
- package/dist/brief/story.d.ts.map +1 -0
- package/dist/brief/story.js +59 -0
- package/dist/brief/types.d.ts +126 -0
- package/dist/brief/types.d.ts.map +1 -0
- package/dist/brief/types.js +11 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +199 -0
- package/dist/cli/init-args.d.ts +16 -0
- package/dist/cli/init-args.d.ts.map +1 -0
- package/dist/cli/init-args.js +24 -0
- package/dist/cli/ui-args.d.ts +23 -0
- package/dist/cli/ui-args.d.ts.map +1 -0
- package/dist/cli/ui-args.js +66 -0
- package/dist/cli/ui.d.ts +40 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +180 -0
- package/dist/coordinatorium/checkpoint.d.ts +44 -0
- package/dist/coordinatorium/checkpoint.d.ts.map +1 -0
- package/dist/coordinatorium/checkpoint.js +124 -0
- package/dist/coordinatorium/cli.d.ts +39 -0
- package/dist/coordinatorium/cli.d.ts.map +1 -0
- package/dist/coordinatorium/cli.js +203 -0
- package/dist/coordinatorium/errors.d.ts +112 -0
- package/dist/coordinatorium/errors.d.ts.map +1 -0
- package/dist/coordinatorium/errors.js +145 -0
- package/dist/coordinatorium/index.d.ts +11 -0
- package/dist/coordinatorium/index.d.ts.map +1 -0
- package/dist/coordinatorium/index.js +5 -0
- package/dist/coordinatorium/lock.d.ts +187 -0
- package/dist/coordinatorium/lock.d.ts.map +1 -0
- package/dist/coordinatorium/lock.js +659 -0
- package/dist/coordinatorium/mission.d.ts +32 -0
- package/dist/coordinatorium/mission.d.ts.map +1 -0
- package/dist/coordinatorium/mission.js +87 -0
- package/dist/coordinatorium/state-keeper.d.ts +115 -0
- package/dist/coordinatorium/state-keeper.d.ts.map +1 -0
- package/dist/coordinatorium/state-keeper.js +396 -0
- package/dist/coordinatorium/types.d.ts +69 -0
- package/dist/coordinatorium/types.d.ts.map +1 -0
- package/dist/coordinatorium/types.js +27 -0
- package/dist/discovery/compose.d.ts +28 -0
- package/dist/discovery/compose.d.ts.map +1 -0
- package/dist/discovery/compose.js +159 -0
- package/dist/discovery/contracts.d.ts +211 -0
- package/dist/discovery/contracts.d.ts.map +1 -0
- package/dist/discovery/contracts.js +555 -0
- package/dist/discovery/docs.d.ts +278 -0
- package/dist/discovery/docs.d.ts.map +1 -0
- package/dist/discovery/docs.js +1182 -0
- package/dist/discovery/facts.d.ts +49 -0
- package/dist/discovery/facts.d.ts.map +1 -0
- package/dist/discovery/facts.js +541 -0
- package/dist/discovery/index.d.ts +8 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +9 -0
- package/dist/discovery/orchestrate.d.ts +35 -0
- package/dist/discovery/orchestrate.d.ts.map +1 -0
- package/dist/discovery/orchestrate.js +32 -0
- package/dist/discovery/render.d.ts +36 -0
- package/dist/discovery/render.d.ts.map +1 -0
- package/dist/discovery/render.js +278 -0
- package/dist/discovery/traverse.d.ts +197 -0
- package/dist/discovery/traverse.d.ts.map +1 -0
- package/dist/discovery/traverse.js +510 -0
- package/dist/durable/assess.d.ts +105 -0
- package/dist/durable/assess.d.ts.map +1 -0
- package/dist/durable/assess.js +239 -0
- package/dist/durable/canonical.d.ts +25 -0
- package/dist/durable/canonical.d.ts.map +1 -0
- package/dist/durable/canonical.js +78 -0
- package/dist/durable/cli.d.ts +37 -0
- package/dist/durable/cli.d.ts.map +1 -0
- package/dist/durable/cli.js +141 -0
- package/dist/durable/errors.d.ts +192 -0
- package/dist/durable/errors.d.ts.map +1 -0
- package/dist/durable/errors.js +323 -0
- package/dist/durable/fs-store.d.ts +254 -0
- package/dist/durable/fs-store.d.ts.map +1 -0
- package/dist/durable/fs-store.js +1493 -0
- package/dist/durable/index.d.ts +20 -0
- package/dist/durable/index.d.ts.map +1 -0
- package/dist/durable/index.js +13 -0
- package/dist/durable/log.d.ts +33 -0
- package/dist/durable/log.d.ts.map +1 -0
- package/dist/durable/log.js +157 -0
- package/dist/durable/memory-store.d.ts +51 -0
- package/dist/durable/memory-store.d.ts.map +1 -0
- package/dist/durable/memory-store.js +302 -0
- package/dist/durable/projection.d.ts +35 -0
- package/dist/durable/projection.d.ts.map +1 -0
- package/dist/durable/projection.js +328 -0
- package/dist/durable/prune-check.d.ts +34 -0
- package/dist/durable/prune-check.d.ts.map +1 -0
- package/dist/durable/prune-check.js +72 -0
- package/dist/durable/register.d.ts +100 -0
- package/dist/durable/register.d.ts.map +1 -0
- package/dist/durable/register.js +532 -0
- package/dist/durable/retention.d.ts +49 -0
- package/dist/durable/retention.d.ts.map +1 -0
- package/dist/durable/retention.js +59 -0
- package/dist/durable/runtime.d.ts +499 -0
- package/dist/durable/runtime.d.ts.map +1 -0
- package/dist/durable/runtime.js +1288 -0
- package/dist/durable/seams.d.ts +44 -0
- package/dist/durable/seams.d.ts.map +1 -0
- package/dist/durable/seams.js +21 -0
- package/dist/durable/testing.d.ts +11 -0
- package/dist/durable/testing.d.ts.map +1 -0
- package/dist/durable/testing.js +25 -0
- package/dist/durable/types.d.ts +442 -0
- package/dist/durable/types.d.ts.map +1 -0
- package/dist/durable/types.js +75 -0
- package/dist/graph/builder.d.ts +11 -0
- package/dist/graph/builder.d.ts.map +1 -0
- package/dist/graph/builder.js +153 -0
- package/dist/graph/identity.d.ts +37 -0
- package/dist/graph/identity.d.ts.map +1 -0
- package/dist/graph/identity.js +46 -0
- package/dist/graph/index.d.ts +7 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +3 -0
- package/dist/graph/resolver.d.ts +41 -0
- package/dist/graph/resolver.d.ts.map +1 -0
- package/dist/graph/resolver.js +36 -0
- package/dist/graph/types.d.ts +42 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/types.js +1 -0
- package/dist/http/index.d.ts +5 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +2 -0
- package/dist/http/server.d.ts +76 -0
- package/dist/http/server.d.ts.map +1 -0
- package/dist/http/server.js +195 -0
- package/dist/http/ui-assets.d.ts +22 -0
- package/dist/http/ui-assets.d.ts.map +1 -0
- package/dist/http/ui-assets.js +100 -0
- package/dist/ideas/cli.d.ts +33 -0
- package/dist/ideas/cli.d.ts.map +1 -0
- package/dist/ideas/cli.js +158 -0
- package/dist/ideas/format.d.ts +38 -0
- package/dist/ideas/format.d.ts.map +1 -0
- package/dist/ideas/format.js +194 -0
- package/dist/ideas/index.d.ts +6 -0
- package/dist/ideas/index.d.ts.map +1 -0
- package/dist/ideas/index.js +3 -0
- package/dist/ideas/store.d.ts +55 -0
- package/dist/ideas/store.d.ts.map +1 -0
- package/dist/ideas/store.js +262 -0
- package/dist/ideas/types.d.ts +54 -0
- package/dist/ideas/types.d.ts.map +1 -0
- package/dist/ideas/types.js +26 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/init/index.d.ts +12 -0
- package/dist/init/index.d.ts.map +1 -0
- package/dist/init/index.js +90 -0
- package/dist/init/prompts.d.ts +3 -0
- package/dist/init/prompts.d.ts.map +1 -0
- package/dist/init/prompts.js +47 -0
- package/dist/init/scaffold.d.ts +29 -0
- package/dist/init/scaffold.d.ts.map +1 -0
- package/dist/init/scaffold.js +208 -0
- package/dist/init/types.d.ts +14 -0
- package/dist/init/types.d.ts.map +1 -0
- package/dist/init/types.js +1 -0
- package/dist/managed/index.d.ts +73 -0
- package/dist/managed/index.d.ts.map +1 -0
- package/dist/managed/index.js +222 -0
- package/dist/readmodel/dashboard.d.ts +40 -0
- package/dist/readmodel/dashboard.d.ts.map +1 -0
- package/dist/readmodel/dashboard.js +236 -0
- package/dist/readmodel/index.d.ts +3 -0
- package/dist/readmodel/index.d.ts.map +1 -0
- package/dist/readmodel/index.js +1 -0
- package/dist/readmodel/types.d.ts +104 -0
- package/dist/readmodel/types.d.ts.map +1 -0
- package/dist/readmodel/types.js +15 -0
- package/dist/root/index.d.ts +186 -0
- package/dist/root/index.d.ts.map +1 -0
- package/dist/root/index.js +430 -0
- package/dist/root/safe-write.d.ts +268 -0
- package/dist/root/safe-write.d.ts.map +1 -0
- package/dist/root/safe-write.js +966 -0
- package/dist/root/write-artifacts.d.ts +20 -0
- package/dist/root/write-artifacts.d.ts.map +1 -0
- package/dist/root/write-artifacts.js +42 -0
- package/dist/scriptorium/compile.d.ts +4 -0
- package/dist/scriptorium/compile.d.ts.map +1 -0
- package/dist/scriptorium/compile.js +185 -0
- package/dist/scriptorium/frontmatter.d.ts +13 -0
- package/dist/scriptorium/frontmatter.d.ts.map +1 -0
- package/dist/scriptorium/frontmatter.js +90 -0
- package/dist/scriptorium/index.d.ts +3 -0
- package/dist/scriptorium/index.d.ts.map +1 -0
- package/dist/scriptorium/index.js +1 -0
- package/dist/scriptorium/markdown.d.ts +31 -0
- package/dist/scriptorium/markdown.d.ts.map +1 -0
- package/dist/scriptorium/markdown.js +88 -0
- package/dist/scriptorium/types.d.ts +78 -0
- package/dist/scriptorium/types.d.ts.map +1 -0
- package/dist/scriptorium/types.js +1 -0
- package/dist/types/enforcement.d.ts +11 -0
- package/dist/types/enforcement.d.ts.map +1 -0
- package/dist/types/enforcement.js +1 -0
- package/dist/types/integrity.d.ts +33 -0
- package/dist/types/integrity.d.ts.map +1 -0
- package/dist/types/integrity.js +1 -0
- package/dist/types/project-definition.d.ts +30 -0
- package/dist/types/project-definition.d.ts.map +1 -0
- package/dist/types/project-definition.js +1 -0
- package/dist/ui/assets/cosmic-glass-bg-Dwx47zPu.webp +0 -0
- package/dist/ui/assets/index-C4NhMaq7.js +28 -0
- package/dist/ui/assets/index-DMZ7ZvZY.css +1 -0
- package/dist/ui/index.html +13 -0
- package/dist/update/cli.d.ts +8 -0
- package/dist/update/cli.d.ts.map +1 -0
- package/dist/update/cli.js +115 -0
- package/dist/update/index.d.ts +78 -0
- package/dist/update/index.d.ts.map +1 -0
- package/dist/update/index.js +285 -0
- package/dist/validate/index.d.ts +6 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +214 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +1 -0
- package/package.json +67 -4
- package/templates/.gitkeep +0 -0
- package/templates/default-instance/instruction/.cursorrules +59 -0
- package/templates/default-instance/instruction/.windsurfrules +59 -0
- package/templates/default-instance/instruction/CLAUDE.md +57 -0
- package/templates/default-instance/managed/agent.md +20 -0
- package/templates/default-instance/managed/bootstrap.md +7 -0
- package/templates/default-instance/managed/index-idea.md +5 -0
- package/templates/default-instance/managed/routing-idea.md +14 -0
- package/templates/default-instance/monolith/AGENT.md +1 -0
- package/templates/default-instance/monolith/context/boot/HANDOFF.md +49 -0
- package/templates/default-instance/monolith/context/boot/WORK_SESSION.md +30 -0
- package/templates/default-instance/monolith/context/execution/current-state.md +30 -0
- package/templates/default-instance/monolith/context/maps/dependency-map.md +38 -0
- package/templates/default-instance/monolith/context/maps/project-map.md +85 -0
- package/templates/default-instance/monolith/context/maps/roadmap-index.md +41 -0
- package/templates/default-instance/monolith/context/maps/routing-map.md +151 -0
- package/templates/default-instance/monolith/context/maps/semantic-map.md +75 -0
- package/templates/default-instance/monolith/context/models/model-registry.md +37 -0
- package/templates/default-instance/monolith/context/models/privacy-classification.md +35 -0
- package/templates/default-instance/monolith/context/templates/plan-template.md +56 -0
- package/templates/default-instance/monolith/governance/README.md +29 -0
- package/templates/default-instance/monolith/governance/RULE_SCHEMA.md +58 -0
- package/templates/default-instance/monolith/learning/INDEX.md +48 -0
- package/templates/default-instance/monolith/learning/injection-map.md +49 -0
- package/templates/default-instance/monolith/learning/taxonomy.md +30 -0
- package/templates/default-instance/monolith/memory/errors/README.md +26 -0
- package/templates/default-instance/monolith/memory/errors/SCHEMA.md +47 -0
- package/templates/default-instance/monolith/memory/errors/TAXONOMY.md +27 -0
- package/templates/default-instance/monolith/memory/long-term/architecture-principles.md +31 -0
- package/templates/default-instance/monolith/memory/long-term/decisions.md +36 -0
- package/templates/default-instance/monolith/memory/long-term/execution-history.md +20 -0
- package/templates/default-instance/monolith/memory/long-term/tech-debt.md +25 -0
- package/templates/default-instance/monolith/memory/working/active.md +41 -0
- package/templates/default-instance/monolith/memory/working/governance-result.md +0 -0
- package/templates/default-instance/monolith/memory/working/integrity-result.md +0 -0
- package/templates/default-instance/monolith/memory/working/story-index.md +28 -0
- package/templates/default-instance/monolith/protocols/boot/boot-sequence.md +59 -0
- package/templates/default-instance/monolith/protocols/cognition/cognitive-modes.md +90 -0
- package/templates/default-instance/monolith/protocols/cognition/dry-run.md +56 -0
- package/templates/default-instance/monolith/protocols/cognition/execution-trace-format.md +52 -0
- package/templates/default-instance/monolith/protocols/cognition/semantic-planning.md +51 -0
- package/templates/default-instance/monolith/protocols/execution/hybrid-execution.md +37 -0
- package/templates/default-instance/monolith/protocols/execution/local-llm-governance.md +42 -0
- package/templates/default-instance/monolith/protocols/governance/command-policy.md +51 -0
- package/templates/default-instance/monolith/protocols/governance/core-invariants.md +40 -0
- package/templates/default-instance/monolith/protocols/governance/memory-governance.md +54 -0
- package/templates/default-instance/monolith/protocols/governance/metadata-schema.md +62 -0
- package/templates/default-instance/monolith/protocols/governance/skills.md +53 -0
- package/templates/default-instance/monolith/protocols/governance/work-session.md +46 -0
- package/templates/default-instance/monolith/skills/INDEX.md +32 -0
- package/templates/default-instance/monolith/skills/current-status/SKILL.md +47 -0
- package/templates/default-instance/monolith/skills/finalize-story/SKILL.md +86 -0
- package/templates/default-instance/monolith/skills/global-status/SKILL.md +56 -0
- package/templates/default-instance/monolith/skills/idea-capture/SKILL.md +157 -0
- package/templates/default-instance/monolith/skills/self-improvement/SKILL.md +77 -0
- package/templates/default-instance/monolith/state.json +11 -0
- package/templates/default-instance/monolith/tests/INDEX.md +57 -0
- package/templates/default-instance/monolith/tests/scenarios/architecture-review.md +32 -0
- package/templates/default-instance/monolith/tests/scenarios/debug-failure.md +32 -0
- package/templates/default-instance/monolith/tests/scenarios/dry-run-activation.md +33 -0
- package/templates/default-instance/monolith/tests/scenarios/execution-trace-verification.md +31 -0
- package/templates/default-instance/monolith/tests/scenarios/finalize-story.md +35 -0
- package/templates/default-instance/monolith/tests/scenarios/implement-feature.md +36 -0
- package/templates/default-instance/monolith/tests/scenarios/local-llm-delegation.md +31 -0
- package/templates/default-instance/monolith/tests/scenarios/planning-request.md +32 -0
- package/templates/default-instance/monolith/tests/scenarios/sensitive-context-task.md +30 -0
- package/templates/default-instance/monolith/tests/scenarios/validator-verification.md +33 -0
- package/templates/default-instance/monolith/tests/validation/expected-behavior.md +70 -0
- package/templates/default-instance/monolith/tests/validators/INDEX.md +17 -0
- package/templates/default-instance/monolith/tests/validators/delegation-semantics.md +22 -0
- package/templates/default-instance/monolith/tests/validators/mode-consistency.md +25 -0
- package/templates/default-instance/monolith/tests/validators/privacy-routing.md +23 -0
- package/templates/default-instance/monolith/tests/validators/protocol-references.md +27 -0
- package/templates/default-instance/monolith/tests/validators/routing-integrity.md +30 -0
- package/templates/default-instance/projectroot/AGENTS.md +111 -0
- package/templates/default-instance/projectroot/monolith.config.json +32 -0
- package/templates/default-instance/projectroot/project.definition.mjs.template +41 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic, deterministic markdown-frontmatter discovery used for both
|
|
3
|
+
* Mandatory Protocols (section 7) and ADR Discovery (section 8). Reuses
|
|
4
|
+
* Scriptorium's existing frontmatter parser (per the brief contract) — does
|
|
5
|
+
* NOT use `compileMarkdownArtifact()` or the ERG graph builder, both of
|
|
6
|
+
* which are explicitly out of scope here.
|
|
7
|
+
*
|
|
8
|
+
* Resilience (review findings F-2/F-3): a single unreadable directory,
|
|
9
|
+
* unreadable file, dangling symlink, symlink cycle, or a configured
|
|
10
|
+
* directory that escapes the project root must never crash the whole brief
|
|
11
|
+
* or silently disappear. Each failure is caught, reported as a diagnostic
|
|
12
|
+
* (forcing Completeness: INCOMPLETE), and discovery continues with
|
|
13
|
+
* whatever else is readable.
|
|
14
|
+
*
|
|
15
|
+
* Symlinks (file or directory entries) are never followed while walking —
|
|
16
|
+
* this is what makes a symlink cycle (ELOOP) impossible by construction,
|
|
17
|
+
* and is also the first line of defense against using a nested symlink to
|
|
18
|
+
* tunnel outside a configured directory. The *top-level* directories named
|
|
19
|
+
* in `dirs` (i.e. `contextGateway.adrDirs` and the standard protocols
|
|
20
|
+
* dir) get an explicit containment check in addition: both a string-level
|
|
21
|
+
* check (rejects `..` segments in the configured path itself) and a
|
|
22
|
+
* real-path check after resolving symlinks (rejects a configured directory
|
|
23
|
+
* that is itself a symlink escaping the project root). Neither check alone
|
|
24
|
+
* is treated as sufficient — path.join()/path.relative() string comparison
|
|
25
|
+
* alone does not account for symlinks on disk.
|
|
26
|
+
*
|
|
27
|
+
* `monolithRoot` itself, and the fixed files below it (state.json,
|
|
28
|
+
* active.md), are vetted separately by the shared resolver in src/root
|
|
29
|
+
* (SEC-001A); this check covers document directories only.
|
|
30
|
+
*/
|
|
31
|
+
import { readdirSync, existsSync, lstatSync, realpathSync } from 'fs';
|
|
32
|
+
import { join, relative, isAbsolute, resolve } from 'path';
|
|
33
|
+
import { readBoundedFileStable } from '../root/index.js';
|
|
34
|
+
import { parseFrontmatter } from '../scriptorium/frontmatter.js';
|
|
35
|
+
import { compareStrings } from './selectors.js';
|
|
36
|
+
/** SEC-001B.1: protocol / ADR documents (largest in this repo is ~80 KB). */
|
|
37
|
+
export const MAX_MARKDOWN_DOC_BYTES = 1024 * 1024;
|
|
38
|
+
const H1_RE = /^#\s+(.+?)\s*$/;
|
|
39
|
+
function errMsg(err) {
|
|
40
|
+
return err instanceof Error ? err.message : String(err);
|
|
41
|
+
}
|
|
42
|
+
/** True if `candidateAbs` is `baseAbs` itself or strictly inside it. */
|
|
43
|
+
function isContained(baseAbs, candidateAbs) {
|
|
44
|
+
const rel = relative(baseAbs, candidateAbs);
|
|
45
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolves `relDir` (relative to targetDir) to an absolute path, rejecting
|
|
49
|
+
* it — with a diagnostic, never silently — if it is not contained within
|
|
50
|
+
* targetDir. See the module doc comment for why both a string-level and a
|
|
51
|
+
* real-path (symlink-resolved) check are performed.
|
|
52
|
+
*/
|
|
53
|
+
function resolveContainedDir(targetDir, relDir, diagnostics) {
|
|
54
|
+
const targetAbs = resolve(targetDir);
|
|
55
|
+
const requestedAbs = resolve(join(targetDir, relDir));
|
|
56
|
+
if (!isContained(targetAbs, requestedAbs)) {
|
|
57
|
+
diagnostics.push({
|
|
58
|
+
code: 'DOC_DIR_OUTSIDE_PROJECT',
|
|
59
|
+
severity: 'error',
|
|
60
|
+
message: `Configured directory "${relDir}" resolves outside the project root and was skipped.`,
|
|
61
|
+
});
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
// `lstat`, not `existsSync`: existsSync follows symlinks, so a DANGLING symlink would
|
|
65
|
+
// read as "absent" and be skipped silently (SEC-001B.1-R, M-1). Only ENOENT on the
|
|
66
|
+
// path itself means "not created yet"; a dangling link falls through to realpath below
|
|
67
|
+
// and is reported as DOC_DIR_UNREADABLE.
|
|
68
|
+
try {
|
|
69
|
+
lstatSync(requestedAbs);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (err.code === 'ENOENT') {
|
|
73
|
+
// Not an error: an ADR/protocol directory that hasn't been created yet is routine.
|
|
74
|
+
return requestedAbs;
|
|
75
|
+
}
|
|
76
|
+
diagnostics.push({
|
|
77
|
+
code: 'DOC_DIR_UNREADABLE',
|
|
78
|
+
severity: 'error',
|
|
79
|
+
message: `"${relDir}" could not be inspected: ${errMsg(err)}`,
|
|
80
|
+
});
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
let realRequested;
|
|
84
|
+
try {
|
|
85
|
+
realRequested = realpathSync(requestedAbs);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
diagnostics.push({
|
|
89
|
+
code: 'DOC_DIR_UNREADABLE',
|
|
90
|
+
severity: 'error',
|
|
91
|
+
message: `"${relDir}" could not be resolved: ${errMsg(err)}`,
|
|
92
|
+
});
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
let realTarget;
|
|
96
|
+
try {
|
|
97
|
+
realTarget = realpathSync(targetAbs);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
realTarget = targetAbs;
|
|
101
|
+
}
|
|
102
|
+
if (!isContained(realTarget, realRequested)) {
|
|
103
|
+
diagnostics.push({
|
|
104
|
+
code: 'DOC_DIR_OUTSIDE_PROJECT',
|
|
105
|
+
severity: 'error',
|
|
106
|
+
message: `Configured directory "${relDir}" escapes the project root via a symlink and was skipped.`,
|
|
107
|
+
});
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return requestedAbs;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* All `.md` files under `dir`, sorted deterministically (not FS traversal
|
|
114
|
+
* order). Never follows symlinks — a symlinked file or directory entry is
|
|
115
|
+
* skipped outright, which also makes a symlink cycle harmless (it is never
|
|
116
|
+
* entered to begin with). Directory read failures (EACCES, ELOOP from a
|
|
117
|
+
* symlink that isn't itself recognized as one by readdir's dirent type,
|
|
118
|
+
* etc.) are reported and that subtree is skipped; sibling subtrees are
|
|
119
|
+
* still walked.
|
|
120
|
+
*/
|
|
121
|
+
function walkMarkdownFilesSorted(dir, targetDir, diagnostics) {
|
|
122
|
+
const results = [];
|
|
123
|
+
if (!existsSync(dir))
|
|
124
|
+
return results;
|
|
125
|
+
function walk(d) {
|
|
126
|
+
let entries;
|
|
127
|
+
try {
|
|
128
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
diagnostics.push({
|
|
132
|
+
code: 'DOC_DIR_UNREADABLE',
|
|
133
|
+
severity: 'error',
|
|
134
|
+
message: `"${relative(targetDir, d)}" could not be read: ${errMsg(err)}`,
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const sorted = entries.slice().sort((a, b) => compareStrings(a.name, b.name));
|
|
139
|
+
for (const entry of sorted) {
|
|
140
|
+
if (entry.isSymbolicLink())
|
|
141
|
+
continue; // never follow symlinks — files or directories
|
|
142
|
+
const full = join(d, entry.name);
|
|
143
|
+
if (entry.isDirectory()) {
|
|
144
|
+
walk(full);
|
|
145
|
+
}
|
|
146
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
147
|
+
results.push(full);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
walk(dir);
|
|
152
|
+
return results.sort(compareStrings);
|
|
153
|
+
}
|
|
154
|
+
function firstH1(bodyLines) {
|
|
155
|
+
for (const line of bodyLines) {
|
|
156
|
+
const match = H1_RE.exec(line.trim());
|
|
157
|
+
if (match)
|
|
158
|
+
return match[1];
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
function parseDoc(absPath, targetDir, diagnostics) {
|
|
163
|
+
const read = readBoundedFileStable(absPath, `"${relative(targetDir, absPath)}"`, { boundary: targetDir, maxBytes: MAX_MARKDOWN_DOC_BYTES, allowEmpty: true }, true);
|
|
164
|
+
if (!read.ok) {
|
|
165
|
+
diagnostics.push({
|
|
166
|
+
code: 'DOC_FILE_UNREADABLE',
|
|
167
|
+
severity: 'error',
|
|
168
|
+
message: `"${relative(targetDir, absPath)}" could not be read: ${read.code}: ${read.detail}`,
|
|
169
|
+
readCode: read.code,
|
|
170
|
+
});
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const content = 'content' in read ? read.content : '';
|
|
174
|
+
const lines = content.split('\n');
|
|
175
|
+
const fm = parseFrontmatter(lines);
|
|
176
|
+
if (!fm)
|
|
177
|
+
return null;
|
|
178
|
+
const fields = new Map();
|
|
179
|
+
let frontmatterTitle;
|
|
180
|
+
let id;
|
|
181
|
+
for (const field of fm.fields) {
|
|
182
|
+
if (field.values[0] !== undefined)
|
|
183
|
+
fields.set(field.key, field.values[0]);
|
|
184
|
+
if (field.key === 'title' && field.values[0])
|
|
185
|
+
frontmatterTitle = field.values[0];
|
|
186
|
+
if (field.key === 'id' && field.values[0])
|
|
187
|
+
id = field.values[0];
|
|
188
|
+
}
|
|
189
|
+
const resolvedTitle = frontmatterTitle ?? firstH1(lines.slice(fm.bodyStartLine));
|
|
190
|
+
return {
|
|
191
|
+
relPath: relative(targetDir, absPath).split('\\').join('/'),
|
|
192
|
+
id,
|
|
193
|
+
resolvedTitle,
|
|
194
|
+
fields,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function resolveTitle(doc) {
|
|
198
|
+
return doc.resolvedTitle ?? doc.id ?? doc.relPath;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Discover documents under `dirs` (relative to targetDir, deduplicated,
|
|
202
|
+
* each walked recursively) whose frontmatter matches `predicate`. Only
|
|
203
|
+
* documents with a usable frontmatter `id:` are included — a markdown file
|
|
204
|
+
* without one isn't a candidate ADR/protocol record, it's silently excluded
|
|
205
|
+
* from this listing (not an error). `diagnostics` collects every resilience
|
|
206
|
+
* finding described in the module doc comment; callers that don't care can
|
|
207
|
+
* omit it.
|
|
208
|
+
*/
|
|
209
|
+
export function discoverDocs(targetDir, dirs, predicate, diagnostics = []) {
|
|
210
|
+
const seenAbsDirs = new Set();
|
|
211
|
+
const refs = [];
|
|
212
|
+
for (const relDir of dirs) {
|
|
213
|
+
const absDir = resolveContainedDir(targetDir, relDir, diagnostics);
|
|
214
|
+
if (!absDir)
|
|
215
|
+
continue;
|
|
216
|
+
if (seenAbsDirs.has(absDir))
|
|
217
|
+
continue;
|
|
218
|
+
seenAbsDirs.add(absDir);
|
|
219
|
+
for (const absPath of walkMarkdownFilesSorted(absDir, targetDir, diagnostics)) {
|
|
220
|
+
const doc = parseDoc(absPath, targetDir, diagnostics);
|
|
221
|
+
if (!doc || !doc.id)
|
|
222
|
+
continue;
|
|
223
|
+
if (!predicate(doc.fields))
|
|
224
|
+
continue;
|
|
225
|
+
refs.push({ id: doc.id, title: resolveTitle(doc), path: doc.relPath });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return refs.sort((a, b) => compareStrings(a.id, b.id) || compareStrings(a.path, b.path));
|
|
229
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project State Freshness (SELF-AWARENESS-001).
|
|
3
|
+
*
|
|
4
|
+
* `Completeness` says whether the brief lost data while being rendered.
|
|
5
|
+
* `Freshness` says whether the recorded project state can still be trusted.
|
|
6
|
+
* They are independent: `Completeness: COMPLETE` + `Freshness: STALE` is a
|
|
7
|
+
* valid combination, and nothing here ever changes `Completeness`.
|
|
8
|
+
*
|
|
9
|
+
* `evaluateFreshness()` is pure: it takes already-gathered evidence and
|
|
10
|
+
* returns a verdict. Git is a signal, never the only source of truth:
|
|
11
|
+
*
|
|
12
|
+
* STALE only on a concrete, named discrepancy — either the state
|
|
13
|
+
* documents contradict each other, or git history references
|
|
14
|
+
* work items that no state document mentions.
|
|
15
|
+
* FRESH only on positive evidence — git is readable, the last state
|
|
16
|
+
* update is located, and every commit after it either touches
|
|
17
|
+
* only the state documents or belongs to a recorded work item.
|
|
18
|
+
* UNKNOWN everything else. Git activity alone, an old date alone, or the
|
|
19
|
+
* absence of an active story alone never produce STALE, and the
|
|
20
|
+
* absence of an active story never means "project complete".
|
|
21
|
+
*/
|
|
22
|
+
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';
|
|
24
|
+
export interface FreshnessReason {
|
|
25
|
+
code: FreshnessReasonCode;
|
|
26
|
+
message: string;
|
|
27
|
+
/** STATE_CONTRADICTION only: the state documents (by file name) that disagree. */
|
|
28
|
+
documents?: string[];
|
|
29
|
+
}
|
|
30
|
+
/** One commit after the last state update, as seen by git. */
|
|
31
|
+
export interface GitCommitSignal {
|
|
32
|
+
sha: string;
|
|
33
|
+
/** ISO-8601 committer date. Informational only — never compared with "now". */
|
|
34
|
+
date: string;
|
|
35
|
+
subject: string;
|
|
36
|
+
/** Paths changed by the commit, relative to the project directory. Empty for plain merges. */
|
|
37
|
+
files: string[];
|
|
38
|
+
}
|
|
39
|
+
export type GitUnavailableReason = 'GIT_NOT_INSTALLED' | 'NOT_A_GIT_REPOSITORY' | 'GIT_FAILED';
|
|
40
|
+
export type GitSignals = {
|
|
41
|
+
status: 'unavailable';
|
|
42
|
+
reason: GitUnavailableReason;
|
|
43
|
+
} | {
|
|
44
|
+
status: 'ok';
|
|
45
|
+
/** The newest commit that touched any state document; null if none ever did. */
|
|
46
|
+
lastStateUpdate: {
|
|
47
|
+
sha: string;
|
|
48
|
+
date: string;
|
|
49
|
+
} | null;
|
|
50
|
+
/** Commits reachable from HEAD but not from `lastStateUpdate`, newest first. */
|
|
51
|
+
commitsSince: GitCommitSignal[];
|
|
52
|
+
/** True when git history was cut at the read limit — the picture is partial. */
|
|
53
|
+
truncated: boolean;
|
|
54
|
+
};
|
|
55
|
+
export type StoryClaim = {
|
|
56
|
+
kind: 'none';
|
|
57
|
+
} | {
|
|
58
|
+
kind: 'ids';
|
|
59
|
+
ids: string[];
|
|
60
|
+
} | {
|
|
61
|
+
kind: 'unknown';
|
|
62
|
+
};
|
|
63
|
+
export interface FreshnessEvidence {
|
|
64
|
+
git: GitSignals;
|
|
65
|
+
/** What `active.md` says is current. `unknown` when the file/section is unreadable or says nothing parseable. */
|
|
66
|
+
activeStory: StoryClaim;
|
|
67
|
+
/** What `roadmap-index.md` says is the active story ("Active Story: ..."), or null if the line is absent. */
|
|
68
|
+
roadmapActiveStory: string | null;
|
|
69
|
+
/** Work-item status by id as declared by roadmap-index.md and story-index.md (uppercased status). */
|
|
70
|
+
roadmapStatuses: ReadonlyMap<string, string>;
|
|
71
|
+
storyIndexStatuses: ReadonlyMap<string, string>;
|
|
72
|
+
/** Concatenated text of the state documents that were readable; used to decide whether an id is "recorded". */
|
|
73
|
+
recordedText: string;
|
|
74
|
+
/** Repo-relative-to-project paths that are state documents (their own commits are not "activity"). */
|
|
75
|
+
stateFiles: ReadonlySet<string>;
|
|
76
|
+
}
|
|
77
|
+
export interface FreshnessResult {
|
|
78
|
+
status: FreshnessStatus;
|
|
79
|
+
reasons: FreshnessReason[];
|
|
80
|
+
/** Human-readable, single-line warnings (never fatal). */
|
|
81
|
+
warnings: string[];
|
|
82
|
+
recommendation: string;
|
|
83
|
+
activeStory: 'none-recorded' | 'recorded' | 'unknown';
|
|
84
|
+
lastStateUpdate: {
|
|
85
|
+
sha: string;
|
|
86
|
+
date: string;
|
|
87
|
+
} | null;
|
|
88
|
+
/** Commits after the last state update that touched something other than state documents. */
|
|
89
|
+
activityCount: number | null;
|
|
90
|
+
/** Work-item ids seen in git after the last state update and absent from every state document. */
|
|
91
|
+
unrecordedWork: string[];
|
|
92
|
+
}
|
|
93
|
+
export declare function extractWorkItemIds(text: string): string[];
|
|
94
|
+
export declare function mentionsId(text: string, id: string): boolean;
|
|
95
|
+
/** Interprets the `## Current Story` excerpt of active.md. */
|
|
96
|
+
export declare function parseActiveStoryClaim(excerpt: string | null): StoryClaim;
|
|
97
|
+
/** `Active Story: <text>` line of roadmap-index.md. */
|
|
98
|
+
export declare function parseRoadmapActiveStory(content: string): string | null;
|
|
99
|
+
/** Table rows `| id | title | STATUS | date |` of roadmap-index.md. */
|
|
100
|
+
export declare function parseRoadmapStatuses(content: string): Map<string, string>;
|
|
101
|
+
/** `### [STORY ]<id> — title` followed by `**Status:** X` in story-index.md. */
|
|
102
|
+
export declare function parseStoryIndexStatuses(content: string): Map<string, string>;
|
|
103
|
+
export declare function evaluateFreshness(ev: FreshnessEvidence): FreshnessResult;
|
|
104
|
+
//# sourceMappingURL=freshness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../../src/brief/freshness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;AAE5D,MAAM,MAAM,mBAAmB,GAC3B,qBAAqB,GACrB,iBAAiB,GACjB,iBAAiB,GACjB,kBAAkB,GAClB,uBAAuB,GACvB,uBAAuB,GACvB,gCAAgC,GAChC,mBAAmB,CAAC;AAExB,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,8DAA8D;AAC9D,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,8FAA8F;IAC9F,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,GAAG,sBAAsB,GAAG,YAAY,CAAC;AAE/F,MAAM,MAAM,UAAU,GAClB;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,GACvD;IACE,MAAM,EAAE,IAAI,CAAC;IACb,gFAAgF;IAChF,eAAe,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACtD,gFAAgF;IAChF,YAAY,EAAE,eAAe,EAAE,CAAC;IAChC,gFAAgF;IAChF,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC;AAEN,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,EAAE,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAExB,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,UAAU,CAAC;IAChB,iHAAiH;IACjH,WAAW,EAAE,UAAU,CAAC;IACxB,6GAA6G;IAC7G,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,qGAAqG;IACrG,eAAe,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,kBAAkB,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD,+GAA+G;IAC/G,YAAY,EAAE,MAAM,CAAC;IACrB,sGAAsG;IACtG,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,eAAe,GAAG,UAAU,GAAG,SAAS,CAAC;IACtD,eAAe,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACtD,6FAA6F;IAC7F,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,kGAAkG;IAClG,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAmBD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAOzD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAG5D;AAQD,8DAA8D;AAC9D,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,UAAU,CAMxE;AAED,uDAAuD;AACvD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGtE;AAED,uEAAuE;AACvE,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAYzE;AAED,gFAAgF;AAChF,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAgB5E;AA8DD,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,iBAAiB,GAAG,eAAe,CA2GxE"}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project State Freshness (SELF-AWARENESS-001).
|
|
3
|
+
*
|
|
4
|
+
* `Completeness` says whether the brief lost data while being rendered.
|
|
5
|
+
* `Freshness` says whether the recorded project state can still be trusted.
|
|
6
|
+
* They are independent: `Completeness: COMPLETE` + `Freshness: STALE` is a
|
|
7
|
+
* valid combination, and nothing here ever changes `Completeness`.
|
|
8
|
+
*
|
|
9
|
+
* `evaluateFreshness()` is pure: it takes already-gathered evidence and
|
|
10
|
+
* returns a verdict. Git is a signal, never the only source of truth:
|
|
11
|
+
*
|
|
12
|
+
* STALE only on a concrete, named discrepancy — either the state
|
|
13
|
+
* documents contradict each other, or git history references
|
|
14
|
+
* work items that no state document mentions.
|
|
15
|
+
* FRESH only on positive evidence — git is readable, the last state
|
|
16
|
+
* update is located, and every commit after it either touches
|
|
17
|
+
* only the state documents or belongs to a recorded work item.
|
|
18
|
+
* UNKNOWN everything else. Git activity alone, an old date alone, or the
|
|
19
|
+
* absence of an active story alone never produce STALE, and the
|
|
20
|
+
* absence of an active story never means "project complete".
|
|
21
|
+
*/
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Work-item ids
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
/**
|
|
26
|
+
* Work-item id: an upper-case, dash-separated prefix, a number, then optional
|
|
27
|
+
* sub-story qualifiers — UI-002D, SEC-001B.1-R, LOCK-RELEASE-001, CONTEXT-001A.
|
|
28
|
+
* Deliberately narrow: an id that is not matched is simply not evidence.
|
|
29
|
+
*/
|
|
30
|
+
const WORK_ITEM_SOURCE = String.raw `(?:[A-Z]{2,}-)+\d{3}[A-Z]?(?:\.\d+)*(?:-[A-Z])?`;
|
|
31
|
+
/** Decision / lesson records are not work items. */
|
|
32
|
+
const NON_WORK_PREFIXES = ['ADR-', 'DEC-', 'LESSON-'];
|
|
33
|
+
function isWorkItem(id) {
|
|
34
|
+
return !NON_WORK_PREFIXES.some((p) => id.startsWith(p));
|
|
35
|
+
}
|
|
36
|
+
export function extractWorkItemIds(text) {
|
|
37
|
+
const re = new RegExp(`(?<![A-Za-z0-9-])${WORK_ITEM_SOURCE}(?![A-Za-z0-9]|\\.\\d|-[A-Z0-9])`, 'g');
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
for (const m of text.matchAll(re)) {
|
|
40
|
+
if (isWorkItem(m[0]))
|
|
41
|
+
seen.add(m[0]);
|
|
42
|
+
}
|
|
43
|
+
return [...seen];
|
|
44
|
+
}
|
|
45
|
+
export function mentionsId(text, id) {
|
|
46
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
47
|
+
return new RegExp(`(?<![A-Za-z0-9-])${escaped}(?![A-Za-z0-9]|\\.\\d|-[A-Z0-9])`).test(text);
|
|
48
|
+
}
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Parsers for the state documents (confirmed structure only)
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
const NONE_RE = /^\W*(?:no active story|none|unknown)\b|^\s*[—-]\s*$/i;
|
|
53
|
+
/** Interprets the `## Current Story` excerpt of active.md. */
|
|
54
|
+
export function parseActiveStoryClaim(excerpt) {
|
|
55
|
+
if (excerpt === null)
|
|
56
|
+
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' };
|
|
62
|
+
}
|
|
63
|
+
/** `Active Story: <text>` line of roadmap-index.md. */
|
|
64
|
+
export function parseRoadmapActiveStory(content) {
|
|
65
|
+
const m = /^Active Story:\s*(.+?)\s*$/m.exec(content);
|
|
66
|
+
return m ? m[1] : null;
|
|
67
|
+
}
|
|
68
|
+
/** Table rows `| id | title | STATUS | date |` of roadmap-index.md. */
|
|
69
|
+
export function parseRoadmapStatuses(content) {
|
|
70
|
+
const out = new Map();
|
|
71
|
+
for (const line of content.split('\n')) {
|
|
72
|
+
const cells = line.split('|').map((c) => c.trim());
|
|
73
|
+
// leading and trailing '|' produce empty first/last cells
|
|
74
|
+
if (cells.length < 5 || cells[0] !== '' || cells[cells.length - 1] !== '')
|
|
75
|
+
continue;
|
|
76
|
+
const id = cells[1];
|
|
77
|
+
const status = cells[3];
|
|
78
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9.\-]*$/.test(id) || !/^[A-Z][A-Z _]+$/.test(status))
|
|
79
|
+
continue;
|
|
80
|
+
if (!out.has(id))
|
|
81
|
+
out.set(id, status);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/** `### [STORY ]<id> — title` followed by `**Status:** X` in story-index.md. */
|
|
86
|
+
export function parseStoryIndexStatuses(content) {
|
|
87
|
+
const out = new Map();
|
|
88
|
+
const lines = content.split('\n');
|
|
89
|
+
for (let i = 0; i < lines.length; i++) {
|
|
90
|
+
const h = /^###\s+(?:STORY\s+)?([A-Za-z0-9][A-Za-z0-9.\-]*)\s+—/.exec(lines[i]);
|
|
91
|
+
if (!h)
|
|
92
|
+
continue;
|
|
93
|
+
for (let j = i + 1; j < Math.min(lines.length, i + 6); j++) {
|
|
94
|
+
if (/^#{1,3}\s/.test(lines[j]))
|
|
95
|
+
break;
|
|
96
|
+
const s = /^\*\*Status:\*\*\s*([A-Z][A-Z _]*)/.exec(lines[j]);
|
|
97
|
+
if (s) {
|
|
98
|
+
if (!out.has(h[1]))
|
|
99
|
+
out.set(h[1], s[1].trim());
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Evaluation
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
const MAX_LISTED = 5;
|
|
110
|
+
function listIds(ids) {
|
|
111
|
+
if (ids.length <= MAX_LISTED)
|
|
112
|
+
return ids.join(', ');
|
|
113
|
+
return `${ids.slice(0, MAX_LISTED).join(', ')} and ${ids.length - MAX_LISTED} more`;
|
|
114
|
+
}
|
|
115
|
+
function isDone(status) {
|
|
116
|
+
return status === 'COMPLETED' || status === 'DONE';
|
|
117
|
+
}
|
|
118
|
+
function findContradictions(ev) {
|
|
119
|
+
const out = [];
|
|
120
|
+
if (ev.activeStory.kind === 'none' && ev.roadmapActiveStory !== null && !NONE_RE.test(ev.roadmapActiveStory)) {
|
|
121
|
+
out.push({
|
|
122
|
+
message: `active.md records no active story, but roadmap-index.md names "${ev.roadmapActiveStory}".`,
|
|
123
|
+
documents: ['active.md', 'roadmap-index.md'],
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (ev.activeStory.kind === 'ids') {
|
|
127
|
+
for (const id of ev.activeStory.ids) {
|
|
128
|
+
if (isDone(ev.roadmapStatuses.get(id))) {
|
|
129
|
+
out.push({
|
|
130
|
+
message: `active.md treats ${id} as current, but roadmap-index.md marks it ${ev.roadmapStatuses.get(id)}.`,
|
|
131
|
+
documents: ['active.md', 'roadmap-index.md'],
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else if (isDone(ev.storyIndexStatuses.get(id))) {
|
|
135
|
+
out.push({
|
|
136
|
+
message: `active.md treats ${id} as current, but story-index.md marks it ${ev.storyIndexStatuses.get(id)}.`,
|
|
137
|
+
documents: ['active.md', 'story-index.md'],
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const [id, roadmapStatus] of ev.roadmapStatuses) {
|
|
143
|
+
const indexStatus = ev.storyIndexStatuses.get(id);
|
|
144
|
+
if (indexStatus !== undefined && indexStatus !== roadmapStatus) {
|
|
145
|
+
out.push({
|
|
146
|
+
message: `${id} is ${roadmapStatus} in roadmap-index.md but ${indexStatus} in story-index.md.`,
|
|
147
|
+
documents: ['roadmap-index.md', 'story-index.md'],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
const REVIEW_RECOMMENDATION = 'Review project state before relying on this brief.';
|
|
154
|
+
export function evaluateFreshness(ev) {
|
|
155
|
+
const reasons = [];
|
|
156
|
+
const warnings = [];
|
|
157
|
+
const base = {
|
|
158
|
+
activeStory: ev.activeStory.kind === 'none' ? 'none-recorded' : ev.activeStory.kind === 'ids' ? 'recorded' : 'unknown',
|
|
159
|
+
lastStateUpdate: ev.git.status === 'ok' ? ev.git.lastStateUpdate : null,
|
|
160
|
+
activityCount: null,
|
|
161
|
+
unrecordedWork: [],
|
|
162
|
+
};
|
|
163
|
+
// 1. Internal contradictions need no git at all.
|
|
164
|
+
const contradictions = findContradictions(ev);
|
|
165
|
+
for (const c of contradictions)
|
|
166
|
+
reasons.push({ code: 'STATE_CONTRADICTION', message: c.message, documents: c.documents });
|
|
167
|
+
// 2. Git-derived signals.
|
|
168
|
+
let gitVerdict = null;
|
|
169
|
+
if (ev.git.status === 'unavailable') {
|
|
170
|
+
const why = {
|
|
171
|
+
GIT_NOT_INSTALLED: 'git is not installed or not runnable',
|
|
172
|
+
NOT_A_GIT_REPOSITORY: 'the project is not inside a git repository',
|
|
173
|
+
GIT_FAILED: 'git could not be queried',
|
|
174
|
+
};
|
|
175
|
+
reasons.push({ code: 'GIT_UNAVAILABLE', message: `Cannot compare state with history: ${why[ev.git.reason]}.` });
|
|
176
|
+
gitVerdict = 'unknown';
|
|
177
|
+
}
|
|
178
|
+
else if (ev.git.lastStateUpdate === null) {
|
|
179
|
+
reasons.push({
|
|
180
|
+
code: 'STATE_NOT_IN_GIT',
|
|
181
|
+
message: 'No commit touches the state documents, so the last state update cannot be located.',
|
|
182
|
+
});
|
|
183
|
+
gitVerdict = 'unknown';
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const activity = ev.git.commitsSince.filter((c) => c.files.some((f) => !ev.stateFiles.has(f)));
|
|
187
|
+
base.activityCount = activity.length;
|
|
188
|
+
const referenced = new Set();
|
|
189
|
+
let unattributed = 0;
|
|
190
|
+
for (const c of activity) {
|
|
191
|
+
const ids = extractWorkItemIds(c.subject);
|
|
192
|
+
if (ids.length === 0)
|
|
193
|
+
unattributed++;
|
|
194
|
+
for (const id of ids)
|
|
195
|
+
referenced.add(id);
|
|
196
|
+
}
|
|
197
|
+
const unrecorded = [...referenced].filter((id) => !mentionsId(ev.recordedText, id)).sort();
|
|
198
|
+
base.unrecordedWork = unrecorded;
|
|
199
|
+
if (unrecorded.length > 0) {
|
|
200
|
+
reasons.push({
|
|
201
|
+
code: 'UNRECORDED_WORK',
|
|
202
|
+
message: `${activity.length} commit(s) after the last state update (${ev.git.lastStateUpdate.sha.slice(0, 7)}, ${ev.git.lastStateUpdate.date.slice(0, 10)}) ` +
|
|
203
|
+
`reference ${unrecorded.length} work item(s) that no state document mentions: ${listIds(unrecorded)}.`,
|
|
204
|
+
});
|
|
205
|
+
gitVerdict = 'stale';
|
|
206
|
+
}
|
|
207
|
+
else if (ev.git.truncated) {
|
|
208
|
+
reasons.push({
|
|
209
|
+
code: 'GIT_HISTORY_TRUNCATED',
|
|
210
|
+
message: 'Git history after the last state update was cut at the read limit; the check is partial.',
|
|
211
|
+
});
|
|
212
|
+
gitVerdict = 'unknown';
|
|
213
|
+
}
|
|
214
|
+
else if (unattributed > 0) {
|
|
215
|
+
reasons.push({
|
|
216
|
+
code: 'UNATTRIBUTED_ACTIVITY',
|
|
217
|
+
message: `${unattributed} commit(s) after the last state update touch project files without naming a work item; ` +
|
|
218
|
+
`whether the recorded state still describes them cannot be determined.`,
|
|
219
|
+
});
|
|
220
|
+
gitVerdict = 'unknown';
|
|
221
|
+
}
|
|
222
|
+
else if (activity.length === 0) {
|
|
223
|
+
reasons.push({
|
|
224
|
+
code: 'NO_ACTIVITY_SINCE_STATE_UPDATE',
|
|
225
|
+
message: 'No commit after the last state update changed anything beyond the state documents.',
|
|
226
|
+
});
|
|
227
|
+
gitVerdict = 'fresh';
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
reasons.push({
|
|
231
|
+
code: 'ACTIVITY_RECORDED',
|
|
232
|
+
message: `All ${activity.length} commit(s) after the last state update reference work items that the state documents mention.`,
|
|
233
|
+
});
|
|
234
|
+
gitVerdict = 'fresh';
|
|
235
|
+
}
|
|
236
|
+
if (activity.length > 0 && ev.activeStory.kind === 'none') {
|
|
237
|
+
warnings.push('Git activity exists after the last recorded state update, but no active story is recorded.');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// 3. Combine. A contradiction or unrecorded work is concrete evidence of staleness;
|
|
241
|
+
// FRESH needs positive evidence and no contradiction.
|
|
242
|
+
let status;
|
|
243
|
+
if (contradictions.length > 0 || gitVerdict === 'stale')
|
|
244
|
+
status = 'STALE';
|
|
245
|
+
else if (gitVerdict === 'fresh')
|
|
246
|
+
status = 'FRESH';
|
|
247
|
+
else
|
|
248
|
+
status = 'UNKNOWN';
|
|
249
|
+
if (status !== 'FRESH') {
|
|
250
|
+
if (status === 'STALE')
|
|
251
|
+
warnings.unshift('Project state may be outdated.');
|
|
252
|
+
else
|
|
253
|
+
warnings.unshift('Freshness could not be established from the available evidence.');
|
|
254
|
+
if (ev.activeStory.kind === 'none') {
|
|
255
|
+
warnings.push('No active story is recorded; this does not mean the project is complete.');
|
|
256
|
+
}
|
|
257
|
+
else if (ev.activeStory.kind === 'unknown') {
|
|
258
|
+
warnings.push('The active story could not be determined from active.md.');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const recommendation = status === 'FRESH' ? 'No action needed.' : status === 'STALE' ? REVIEW_RECOMMENDATION : `${REVIEW_RECOMMENDATION} Freshness is unverified, not confirmed outdated.`;
|
|
262
|
+
return { status, reasons, warnings, recommendation, ...base };
|
|
263
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only git signals for the freshness check (SELF-AWARENESS-001).
|
|
3
|
+
*
|
|
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.
|
|
8
|
+
*/
|
|
9
|
+
import type { GitSignals } from './freshness.js';
|
|
10
|
+
/** Upper bound on commits read after the last state update. Hitting it marks the result truncated. */
|
|
11
|
+
export declare const MAX_COMMITS_SINCE = 500;
|
|
12
|
+
/**
|
|
13
|
+
* @param targetDir project directory (git is run from here; paths are relative to it)
|
|
14
|
+
* @param stateFiles state documents, relative to `targetDir`. Only existing ones need be passed.
|
|
15
|
+
*/
|
|
16
|
+
export declare function readGitSignals(targetDir: string, stateFiles: string[]): GitSignals;
|
|
17
|
+
//# sourceMappingURL=git-signals.d.ts.map
|
|
@@ -0,0 +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"}
|